I'm building a simple Vuejs website in which you can write notes about meetings. Upon loading it takes the meeting notes from the server and displays them. When the user then writes something he can click the "Save" button, which saves the text to the server. When the notes are saved to the server the Save-button needs to be disabled and display a text saying "Saved". When the user then starts writing text again it should enable the button again and display "Save" again. This is a pretty basic functionality I would say, but I'm having trouble with it.
Here's my textarea and my save button:
<textarea v-model="selectedMeeting.content" ref="meetingContent"></textarea>
<button v-on:click="saveMeeting" v-bind:disabled="meetingSaved">
{{ saveMeetingButton.saveText }}
</button>
In my Vue app I first initiate my data:
data: {
selectedMeeting: {},
meetings: [],
meetingSaved: true,
saveMeetingButton: {saveText: 'Save Meeting', savedText: 'Saved', disabled: true},
},
Upon creation I get the meeting notes from the server:
created() {
axios.get('/ajax/meetings')
.then(response => {
this.meetings = response.data;
this.selectedMeeting = this.meetings[0];
this.meetingSaved = true;
});
},
I've got a method to save the notes:
methods: {
saveMeeting: function () {
axios.post('/ajax/meetings/' + this.selectedMeeting.id, this.selectedMeeting)
.then(function (response) {
this.selectedMeeting = response.data;
console.log('Now setting meetingSaved to true');
this.meetingSaved = true;
console.log('Done setting meetingSaved to true');
});
},
},
And I've got a watcher in case something changes to the text which saves the text immediately (this saves with every letter I type, which I of course need to change, but this is just to get started.
watch: {
'selectedMeeting.content': function () {
this.meetingSaved = false;
console.log('Changed meeting ', new Date());
this.saveMeeting();
}
},
If I now type a letter I get this in the logs:
Changed meeting Tue Dec 04 2018 19:14:43 GMT+0100
Now setting meetingSaved to true
Done setting meetingSaved to true
The logs are as expected, but the button itself is never disabled. If I remove the watcher the button is always disabled however. Even though the watcher first sets this.meetingSaved to false, and then this.saveMeeting() sets it to true, adding the watcher somehow never disables the button.
What am I doing wrong here?
Edit
Here's a paste of the whole page: https://pastebin.com/x4VZvbr5
You've got a few things going on that could use some changing around.
Firstly the data attribute should be a function that returns an object:
data() {
return {
selectedMeeting: {
content: null
},
meetings: [],
meetingSaved: true,
saveMeetingButton: {
saveText: 'Save Meeting',
savedText: 'Saved',
disabled: true
},
};
}
This is so Vue can properly bind the properties to each instance.
Also, the content property of the selectedMeeting didn't exist on the initial render so Vue has not added the proper "wrappers" on the property to let things know it updated.
As an aside, this can be done with Vue.set
Next, I would suggest you use async/await for your promises as it makes it easier to follow.
async created() {
const response = await axios.get('/ajax/meetings');
this.meetings = response.data;
this.selectedMeeting = this.meetings[0];
this.meetingSaved = true;
},
For your method I would also write as async/await. You can also use Vue modifiers like once on click to only call the api if there is no previous request (think a fast double-click).
methods: {
async saveMeeting () {
const response = await axios.post('/ajax/meetings/' + this.selectedMeeting.id, this.selectedMeeting);
this.selectedMeeting = response.data;
console.log('Now setting meetingSaved to true');
this.meetingSaved = true;
console.log('Done setting meetingSaved to true');
},
},
The rest of the code looks okay.
To summarize the main problem is that you didn't return an object in the data function and it didn't bind the properties reactively.
Going forward you are going to want to debounce the text input firing the api call and also throttle the calls.
this.meetingSaved = true;
this is referencing axios object. Make a reference to vue object outside your call and than use it. Same happens when you use jQuery.ajax().
created() {
var vm = this;
axios.get('/ajax/meetings')
.then(response => {
vm.meetings = response.data;
vm.selectedMeeting = vm.meetings[0];
vm.meetingSaved = true;
});
},
Related
Context description
I have a Vue 3 project and using element plus, I created a form with async validation. The code of validation function (Laravel precognition):
const validatePrecognitive = async (rule: InternalRuleItem, value: Value, callback: (error?: string | Error) => void) => {
return new Promise<void>((resolve, reject) => {
if (!rule.field) {
return;
}
precognitive.post('/events', computedFormData.value, {
validate: [rule.field],
onPrecognitionSuccess(response) {
if (response.status === 204) {
callback();
resolve();
}
},
onValidationError(response) {
callback(new Error(response.data.message));
reject();
},
});
});
};
and I define the rules like this:
const rules: FormRules = reactive({
} as Partial<Record<string, Arrayable<FormItemRule>>>);
// dynamically set the rules for each input, because of same value for all rules
Object.keys(getSubmitShape()).forEach(key => {
rules[key] = [{ asyncValidator: validatePrecognitiveDebounced, trigger: 'blur' }];
});
The getSubmitShape() funtion return Object with all form-item keys, that should be validated. So you can imagine it like:
const rules: FormRules = reactive({
rules['name'] = [{ asyncValidator: validatePrecognitive, trigger: 'blur' }];
rules['surname'] = [{ asyncValidator: validatePrecognitive, trigger: 'blur' }];
}
This works like a charm, when I write to inputs and switch between them. The errors would appear and dissappear correctly.
Submit
When the form is submitted, I am calling formEl.validate(...) function, which should validate all the inputs. It works, BUT:
for every rule item, one HTTP request is sent to server, because the validation method is called for each rule separately.
What I tried
I tried to implement debounce method, which would save the params from each previous function call and include that in next call, so in a last call, I would have all the inputs, rules and callbacks available. This works, when I try it while writing inputs to elements and switching them fast (triggering validation).
When on submit, it waits the debounce time for each rule. So the requests again go one by one with the delay of debounce time.
Any ideas, how I could make this in one request?
Thanks.
I am trying to change button to saving state while I run code to get information.
I have
this.setState({ saving: true }, () => this.save(event) })
In this.save I have a rest call. I can see from the log that the state is updated but visually on the site the button does not go into the spinning circle like it should with that updated value.
Is there a way to force update rendering before running the callback function or a better method to set a button to saving while I do a remote call that could take a little bit of time?
There is no reason to force this. Change your state in parallel to the actual saving:
<button onClick={() => this.save()}>save</button>
paired with:
save() {
this.setState({ saving: true });
remoteAPI.save({
data: this.getSaveData(),
credentials: this.getCredentials()
...
}, response => {
this.setState({ saving: false });
if(response.error) {
// ohnoes!
} else {
// nice.
}
});
}
I'm trying to run a function that needs some data that I get back from the mounted method. Right now I try to use computed to create the function but unfortunately for this situation computed runs before mounted so I don't have the data I need for the function. Here is what I'm working with:
computed: {
league_id () {
return parseInt(this.$route.params.id)
},
current_user_has_team: function() {
debugger;
}
},
mounted () {
const params = {};
axios.get('/api/v1/leagues/' +this.$route.params.id, {
params,
headers: {
Authorization: "Bearer "+localStorage.getItem('token')
}
}).then(response => {
debugger;
this.league = response.data.league
this.current_user_teams = response.data.league
}).catch(error => {
this.$router.push('/not_found')
this.$store.commit("FLASH_MESSAGE", {
message: "League not found",
show: true,
styleClass: "error",
timeOut: 4000
})
})
}
As you can see I have the debugger in the computed function called current_user_has_team function. But I need the data I get back from the axios call. Right now I don't have the data in the debugger. What call back should I use so that I can leverage the data that comes back from the network request? Thank You!
If your computed property current_user_has_team depends on data which is not available until after the axios call, then you need to either:
In the current_user_has_team property, if the data is not available then return a sensible default value.
Do not access current_user_has_team from your template (restrict with v-if) or anywhere else until after the axios call has completed and the data is available.
It's up to you how you want the component to behave in "loading" situations.
If your behavior is synchronous, you can use beforeMount instead of mounted to have the code run before computed properties are calculated.
I am using ember. I intercept one component's button click in controller. The click is to trigger a new report request. When a new report request is made, I want the newly made request to appear on the list of requests that I currently show. How do I make ember refresh the page without obvious flicker?
Here is my sendAction code:
actions: {
sendData: function () {
this.set('showLoading', true);
let data = {
startTime: date.normalizeTimestamp(this.get('startTimestamp')),
endTime: date.normalizeTimestamp(this.get('endTimestamp')),
type: constants.ENTERPRISE.REPORTING_PAYMENT_TYPE
};
api.ajaxPost(`${api.buildV3EnterpriseUrl('reports')}`, data).then(response => {
this.set('showLoading', false);
return response.report;
}).catch(error => {
this.set('showLoading', false);
if (error.status === constants.HTTP_STATUS.GATEWAY_TIMEOUT) {
this.notify.error(this.translate('reports.report_timedout'),
this.translate('reports.report_timedout_desc'));
} else {
this.send('error', error);
}
});
}
There are few think you should consider. Generaly you want to have variable that holds an array which you are render in template in loop. For example: you fetch your initial set of data in route and pass it on as model variable.
// route.js
model() { return []; }
// controller
actions: {
sendData() {
foo().then(payload => {
// important is to use pushObjects method.
// Plain push will work but wont update the template.
this.get('model').pushObjects(payload);
});
}
}
This will automatically update template and add additional items on the list.
Boilerplate for showLoading
You can easily refactor your code and use ember-concurency. Check their docs, afair there is example fitting your usecase.
I have a "box" route/controller as below;
export default Ember.Controller.extend({
initialized: false,
type: 'P',
status: 'done',
layouts: null,
toggleFltr: null,
gridVals: Ember.computed.alias('model.gridParas'),
gridParas: Ember.computed('myServerPars', function() {
this.set('gridVals.serverParas', this.get('myServerPars'));
this.filterCols();
if (!this.get('initialized')) {
this.toggleProperty('initialized');
} else {
Ember.run.scheduleOnce('afterRender', this, this.refreshBox);
}
return this.get('gridVals');
}),
filterCols: function()
{
this.set('gridVals.layout', this.get('layouts')[this.get('type')]);
},
myServerPars: function() {
// Code to set serverParas
return serverParas;
}.property('type', 'status', 'toggleFltr'),
refreshBox: function(){
// Code to trigger refresh grid
}
});
My route looks like;
export default Ember.Route.extend({
selectedRows: '',
selectedCount: 0,
rawResponse: {},
model: function() {
var compObj = {};
compObj.gridParas = this.get('gridParas');
return compObj;
},
activate: function() {
var self = this;
self.layouts = {};
var someData = {attr1:"I"};
var promise = this.doPost(someData, '/myService1', false); // Sync request (Is there some way I can make this work using "async")
promise.then(function(response) {
// Code to use response & set self.layouts
self.controllerFor(self.routeName).set('layouts', self.layouts);
});
},
gridParas: function() {
var self = this;
var returnObj = {};
returnObj.url = '/myService2';
returnObj.beforeLoadComplete = function(records) {
// Code to use response & set records
return records;
};
return returnObj;
}.property(),
actions: {
}
});
My template looks like
{{my-grid params=this.gridParas elementId='myGrid'}}
My doPost method looks like below;
doPost: function(postData, requestUrl, isAsync){
requestUrl = this.getURL(requestUrl);
isAsync = (isAsync == undefined) ? true : isAsync;
var promise = new Ember.RSVP.Promise(function(resolve, reject) {
return $.ajax({
// settings
}).success(resolve).error(reject);
});
return promise;
}
Given the above setup, I wanted to understand the flow/sequence of execution (i.e. for the different hooks).
I was trying to debug and it kept hopping from one class to another.
Also, 2 specific questions;
I was expecting the "activate" hook to be fired initially, but found out that is not the case. It first executes the "gridParas" hook
i.e. before the "activate" hook. Is it because of "gridParas"
specified in the template ?
When I do this.doPost() for /myService1, it has to be a "sync" request, else the flow of execution changes and I get an error.
Actually I want the code inside filterCols() controller i.e.
this.set('gridVals.layout', this.get('layouts')[this.get('type')]) to
be executed only after the response has been received from
/myService1. However, as of now, I have to use a "sync" request to do
that, otherwise with "async", the execution moves to filterCols() and
since I do not have the response yet, it throws an error.
Just to add, I am using Ember v 2.0
activate() on the route is triggered after the beforeModel, model and afterModel hooks... because those 3 hooks are considered the "validation phase" (which determines if the route will resolve at all). To be clear, this route hook has nothing to do with using gridParas in your template... it has everything to do with callling get('gridParas') within your model hook.
It is not clear to me where doPost() is connected to the rest of your code... however because it is returning a promise object you can tack on a then() which will allow you to essentially wait for the promise response and then use it in the rest of your code.
Simple Example:
this.doPost().then((theResponse) => {
this.doSomethingWith(theResponse);
});
If you can simplify your question to be more clear and concise, i may be able to provide more info
Generally at this level you should explain what you want to archive, and not just ask how it works, because I think you fight a lot against the framework!
But I take this out of your comment.
First, you don't need your doPost method! jQuerys $.ajax returns a thenable, that can be resolved to a Promise with Ember.RSVP.resolve!
Next: If you want to fetch data before actually rendering anything you should do this in the model hook!
I'm not sure if you want to fetch /service1, and then with the response you build a request to /service2, or if you can fetch both services independently and then show your data (your grid?) with the data of both services. So here are both ways:
If you can fetch both services independently do this in your routes model hook:
return Ember.RSVP.hash({
service1: Ember.RSVP.resolve($.ajax(/*your request to /service1 with all data and params, may use query-params!*/).then(data => {
return data; // extract the data you need, may transform the response, etc.
},
service2: Ember.RSVP.resolve($.ajax(/*your request to /service2 with all data and params, may use query-params!*/).then(data => {
return data; // extract the data you need, may transform the response, etc.
},
});
If you need the response of /service1 to fetch /service2 just do this in your model hook:
return Ember.RSVP.resolve($.ajax(/*/service1*/)).then(service1 => {
return Ember.RSVP.resolve($.ajax(/*/service2*/)).then(service2 => {
return {
service1,
service2
}; // this object will then be available as `model` on your controller
});
});
If this does not help you (and I really think this should fix your problems) please describe your Problem.