0

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;
    })
}

1 Answer 1

0

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 like me.$refs.loader.hide(); suggest program state changes that should be controlled by setting a loaderIsVisible data item, for example.

Sign up to request clarification or add additional context in comments.

4 Comments

Yup. When we do this, most often, it's so we can return a promise. That's where I'm blocking for the moment. How does the caller find whether our user confirmed or cancelled the prompt? (The loader example is a bit different. It's got a little bit of logic to appear a second after it's called, and to be cancellable during this interval. There's always some little thing!)
I'm really quite pleased with my tunnel expressed as a function. It's imperative because order is important. But it doesn't feel right usurping the store, and having all that sitting out in a Vue component. I could implement it with each little sequence modifying the store, thus triggering the next little sequence. Would that be better? The ordering of the steps wouldn't be expressed anywhere. It would be the kinda magical result of a number of watch functions. Perhaps I'm trying to reinvent the controller?
The tunnel as a function is ok, as far as I can tell. The point is that, as you say, each sequence should modify the store, so that the store triggers all the behavior. Anything you trigger by other means is a bit of state that is not in the store. If the point of the store is to be a complete representation of program state, you need to think of everything in terms of what you do in the store.
Yup. I was getting to there. Even if the sequencing methods stay where they are in Vue components, if they modify the store, rather than directly controlling visual elements, that will be a step forward. It still leaves me with important state in the promises and their state of advancement. I'll leave the question open for any brainwaves but thanks for your consideration.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.