We jumped into Vue, and totally loved the idea of a store, separating page state from presentation. We wrote a zippy alert component, that displayed whatever alert string was in store.pageError.
Then we wanted to add confirm(), so we gave our component a ref, and a popConfirm() method that returned a promise, and whenever we wanted to confirm something, we called...
vm.$refs.alertConfirm.confirm("Are you sure?")
.then(function(conf){if(conf) /* do it */ })
...and the component became responsible for managing its own visibility, without anything happening in the store.
This worked so well that soon lots of components started sprouting refs, so they could be called directly as methods. In the latest incarnation, we implemented a tunnel with six steps, with api calls and user actions called in parallel with Promise.all(), and it works great, but the store has taken a big step back, and more and more state is being managed directly in Vue components. These components are no longer dumb representations of store state, but increasingly little functional sequences with state managed internally.
How do we reassert the idea of a store, while keeping the convenience of calling these short functional sequences as methods?
Here is our tunnel code. This currently lives in the methods
of a Vue component, where markup, state, and sequential logic are joyously mixed. This can't be good?
startTunnel(idNote) {
var rslt = {
idNote: idNote,
photoJustif: null,
date: null,
currency: "",
montant: null
}
//---------------Step 1: photo et count notes in parallel
Promise.all([
me.$refs.camera.click(),
getUrlAsJson("api/note/getNotes"),
])
//---------------Step 2: Choose note if > 1
.then(function (results) {
rslt.photoJustif = results[0];
me.$refs.loader.hide();
// if we already know the note, go straight to Step 3.
if (rslt.idNote)
return true;
// if no idNote supplied, and only one returned from server, assign it.
if (results[1].notes.length === 1 && !rslt.idNote) {
rslt.idNote = results[1].notes[0].idNote;
return true;
}
else {
return me.$refs.chooseNote.choose(results[1].notes)
// combine photoJustif from Step 1 with idNote chosen just above.
.then(function (idNoteChosen) { rslt.idNote = idNoteChosen; return true })
}
})
//--------------Step 3: OCR
.then(() => me.doOcr(rslt))
//--------------Step 4: Choose nature and retrieve card statement from server in parallel
.then(function (ocrResult) {
me.$refs.loader.hide()
if (ocrResult != null) { //Si ocr n'a pas échoué
rslt.date = ocrResult.date;
rslt.montant = ocrResult.montant;
rslt.currency = ocrResult.currency;
return Promise.all([
me.$refs.chooseNature.init(rslt.idNote, ocrResult.grpNatures),
getUrlAsJson("api/expense/relevecarte/filterpers", { IdPerson: 1, montant: ocrResult.montant })
]);
}
else return null;
})
//--------------Step 5: Choose card transaction
.then(function (natureAndFraisCartes) {
if (natureAndFraisCartes != null) {
rslt.idNature = natureAndFraisCartes[0].id;
if (rslt.montant != null && natureAndFraisCartes[1].length > 1)
return me.$refs.choixFraisCarte.init(rslt, natureAndFraisCartes[1]);
else
return null;
}
else return null;
})
//------------- Step 6: End tunnel
.then(function (fraisCarte) {
me.$refs.loader.popInstant();
me.$refs.form.idNote.value = rslt.idNote;
var jsonObject;
if (fraisCarte != null) {
me.$refs.form.action.value = 15;
jsonObject = {
"DateFrais": rslt.date,
"IdNature": rslt.idNature,
"MontantTicket": rslt.montant,
"Justificatif": rslt.photoJustif,
"idCarte": fraisCarte.id
};
}
else {
me.$refs.form.action.value = 14;
jsonObject = {
"DateFrais": rslt.date,
"IdNature": rslt.idNature,
"MontantTicket": rslt.montant,
"Justificatif": rslt.photoJustif,
"idCarte": 0
};
}
me.$refs.form.obj.value = JSON.stringify(jsonObject);
me.$refs.form.submit();
})
.catch(function (error) {
me.$refs.loader.hide();
me.active = false;
me.rslt = {
idNote: idNote,
photoJustif: null,
date: null,
montant: null
};
console.log(error);
vueStore.pageError = me.allStrings.tunnelPhoto.erreurTunnel;
})
}
It looks like the problem is that you got away from thinking declaratively and went to thinking imperatively. When you want to confirm something, you should set a
confirmPrompt
data item, and the component should be watching it in much the same way it watches the alert string.There should be a data item for the confirmation response to indicate whether you're waiting for a response, or it was confirmed or it was canceled. It's all program state.
Using
$refs
is a code smell. It's not always wrong, but you should always think about why you're doing it. Things likeme.$refs.loader.hide();
suggest program state changes that should be controlled by setting aloaderIsVisible
data item, for example.