1

I have a component called countdowntimer.vue which is obviously just a countdown timer, I use it for an online examination page, I want to apply an onbeforeunload event on the window object but I also want the timer to submit automatically when it's done without being interrupted by that window event, I tried putting the code in the vuejs component but it just doesn't respond, it either doesn't allow me to submit without interrupting, or it doesn't work at all and lets any event get out of the page without interrupting it.

Here's the code for the countdown timer:

<template>
    <div>
        <div v-if="finished" v-text="expiredText"></div>

        <div v-else>
            <span>{{ remaining.minutes }} Minutes, </span>
            <span>{{ remaining.seconds }} Seconds</span>
            left...
        </div>
    </div>
</template>

<script>
    import moment from 'moment';
    export default {
        props: {
            until: { default: 600000},
            expiredText: { default: 'TIME IS UP' }
        },
        data () {
            return { limiter: this.until * 10000};
        },
        created () {
            this.refreshEverySecond();
            document.addEventListener('beforeunload', this.redirect());
        },
        computed: {
            finished () {
                return this.remaining.total <= 0;
            },
            remaining () {
                let remaining = moment.duration(this.limiter);
                if (remaining <= 0) this.$emit('finished');
                return {
                    total: remaining,
                    minutes: remaining.minutes(),
                    seconds: remaining.seconds()
                };
            },
        },
        methods: {
            refreshEverySecond () {
                let interval = setInterval(() => this.limiter = this.limiter - 1000, 1000);
                this.$on('finished', () => clearInterval(interval));
                this.$on('finished', () => this.timeUp());
            },

            timeUp() {
                const form = document.querySelector('[data-form-submit]');
                const radios = document.querySelectorAll('input[type=radio]');
                radios.forEach(radio => radio.style.display = 'none');
                form.submit(function(e) {
                    console.log(e);
                });
            },
            redirect () {
                // if(this.$on('finished')) {
                    // console.log(this.finished)
                    // window.onbeforeunload = function() {
                    //     return "Do you really want to leave our brilliant application?";
                    // };
                // }
                // console.log(this.finished())
                // return;
            }
        },
    }
</script>

I have tried setting the method as a computed property and as a watcher with different if statements but it just doesn't work as I mentioned above.

And here's the blade template I am using it in

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-6 col-md-offset-3">
                <form method="POST" onsubmit="clicked()" data-form-submit>
                    {{ csrf_field() }}
                    <div class="panel panel-danger">
                        <div class="panel-heading">
                            {{ $quiz->quiz_name }}
                        </div>
                        <div class="panel-body">
                            <ol>
                                @foreach($quiz->questions as $index => $question)
                                    <li><h4>{{ $question->title }} ? </h4></li>
                                    <ul>
                                        <div class="flex-align-baseline">
                                            <li>{{$question->option_1}}</li>
                                            <input type="radio" name="{{$index}}" value="{{ $question->option_1 }}">
                                            <hr>
                                        </div>
                                        <div class="flex-align-baseline">
                                            <li>{{$question->option_2}}</li>
                                            <input type="radio" name="{{$index}}" value="{{ $question->option_2 }}">
                                            <hr>
                                        </div>
                                        <div class="flex-align-baseline">
                                            <li>{{$question->option_3}}</li>
                                            <input type="radio" name="{{$index}}" value="{{ $question->option_3 }}">
                                            <hr>
                                        </div>
                                        <div class="flex-align-baseline">
                                            <li>{{$question->option_4}}</li>
                                            <input type="radio" name="{{$index}}" value="{{ $question->option_4 }}">
                                            <hr>
                                        </div>
                                    </ul>
                                    <hr>
                                @endforeach
                            </ol>
                            <input type="submit" class="btn btn-success" onclick="clicked()" value="Submit">
                        </div>
                    </div>
                </form>
            </div>
            <timer :until="{{ count($quiz->questions) }}" class="countdown col-md-3"></timer>
        </div>
    </div>

@endsection

<script>

let submitForm = false;
function clicked() {
    submitForm = true;
}
window.onbeforeunload = function(e) {
    if (submitForm) {
        return null;
    }
    return "Do you really want to leave our brilliant application?";
};
</script>

As you can see I have a script tag outside of the @endsection, what I came to understand is that you probably can't do that, it won't connect to any element from the blade template itself, I tried to grab the form object like I was doing in the vue component but it returns null or undefined I can't remember, and you can't attach an event listener to undefined, but if I run the same logic in the console of the browser it works as expected, the onsubmit="" event that I have on the form doesn't not reach those script tags at the bottom for some reason, the value of the submitForm variable doesn't change, but weirdly enough if I clicked the submit button manually it does trigger the function clicked(), so I am very confused here, I don't know if I can achieve this with just vue or not, and if not I don't know why the onsubmit="" event is not working, and of course I cannot move the script tags inside the @section because vue will squack, if you have any idea what I should do with this bit of code I'd be grateful.

1 Answer 1

2

First off, you should pass a method reference to beforeunload, not the result of calling that method. So, remove the ():

created () {
    this.refreshEverySecond();
    document.addEventListener('beforeunload', this.redirect); // not this.redirect()
},

Now, a simple solution to enable/disable the handler is to just add a flag:

    data () {
        return {
            limiter: this.until * 10000
            preventSubmit: true
        };
    },

And, in your methods, update/use that flag:

    methods: {
        // ...
        timeUp() {
            this.preventSubmit = false; // ALLOW redirect now

            const form = document.querySelector('[data-form-submit]');
            const radios = document.querySelectorAll('input[type=radio]');
            radios.forEach(radio => radio.style.display = 'none');
            form.submit(function(e) {
                console.log(e);
            });
        },
        redirect () {
            if (this.preventSubmit) {
               // do your thing to prevent submit
            }
        }
    },

Alternative

Alternatively, you could just remove the listener:

created () {
    this.refreshEverySecond();
    document.addEventListener('beforeunload', this.redirect); // not this.redirect()
},

And:

methods: {
    // ...
    timeUp() {
        document.removeEventListener('beforeunload', this.redirect);
        // ...

But I think the flag alternative is more fail-safe.


Correctly dealing with the unbeforeunload

Per comments, I'm adding a demo of how it would work.

See JSFiddle DEMO here or demo below.

new Vue({
  el: '#app',
  data: {
    preventSubmit : true
  },
  mounted () {
    window.addEventListener("beforeunload", this.redirect);
	},
  methods: {
  	redirect(event) {
    	if (this.preventSubmit) {
		  	var confirmationMessage = "\o/";
  			event.returnValue = confirmationMessage;     // Gecko, Trident, Chrome 34+
  			return confirmationMessage;              // Gecko, WebKit, Chrome <34
      }
    }
  }
})
<script src="https://unpkg.com/vue"></script>

<div id="app">
  <p>preventSubmit ? {{ preventSubmit  }}</p>
  <button @click="preventSubmit = !preventSubmit ">Toggle preventSubmit </button>
</div>
<br>
<a href="/somewhere-else">click to try to navigate away</a>

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

3 Comments

Right now there's a problem, the document event listener "beforeunload" is not triggering, therefore prohibiting me from testing this correctly, I don't know why the event listener is not capturing that unload event, any thoughts?
Navigating away isn't triggering it?
No, even changed "document" to "window" and still nothing, but of course if I changed the event I'm listening to, let's say click, everything triggers normally.

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.