Our contacts view displays well on desktop, but is less than satisfactory on small screens. We want to show only the contact view and hide the list of contacts and new contact form when a contact is viewed on small screens. There are a variety of ways I could achieve that e.g. detect screen size when a request is made and return a view created for mobile from the server (this is how Amazon works, for example), track changes in screen size and adjust the views accordingly (this is how Twitter mobile works), track the value of the CSS display property on an element etc. Since I don't want to create separate views and the proposed changes are minor, I'll use the third approach.

First, we'll create a mixin that will handle our checks for mobile in resources/js/components/mixins/MobileDetect.vue:

<script>
export default {
  name: 'MobileDetect',
  data() {
    return {
      displayProp: ''
    };
  },
  computed: {
    isMobile() {
      return this.displayProp === 'block';
    }
  },
  mounted() {
    this.updateDisplayProp();
  },
  methods: {
    updateDisplayProp() {
      const el = document.getElementById('mobileDetect');

      if (el) {
        this.displayProp = getComputedStyle(el, null).display;
      }

      setTimeout(() => this.updateDisplayProp(), 5000);
    }
  }
};
</script>

The element referenced resides in ContactView.vue which we'll meet later, but here it is to aid explanation:

<span id="mobileDetect" class="d-md-none d-lg-none d-sm-inline d-xs-inline"></span>

The applied Bootstrap classes will make the span have a CSS display property of none on desktop and block on small screens. To simulate responsiveness, we will check this property periodically (instead of once on page load). That's what happens in updateDisplayProp(). We find the element, get the current value of the display style property and store it in displayProp, and schedule another call. I chose a 5-second interval as the app doesn't do much. If the app performed a lot of computations, we would have chosen a longer interval or settled for calling only once on page load. We use a computed property isMobile to track changes to displayProp.

Now with the core, we can modify the affected components. When we're viewing a contact on small screens we need a way to go back to our contact list, so we'll add a back button.

Update ContactView.vue to:

<template>
  <div class="row justify-content-center">
    <div class="col-md-4 col-sm-12">
      <div v-show="!contactSelectedOnMobile">
        <create-contact @contact-created="handleContactCreated" />
        <contact-list
          @contact-deleted="handleContactDeleted"
          @contact-selected="handleContactSelected"
          :contacts="localContacts" />
      </div>
    </div>
    <div class="col-md-8 col-sm-12">
      <button v-if="contactSelectedOnMobile" class="btn btn-outline-secondary mb-3" @click="resetSelectedContact()">Back</button>
      <contact :contact="selectedContact" @contact-updated="handleContactUpdated" />
    </div>
    <span id="mobileDetect" class="d-md-none d-lg-none d-sm-inline d-xs-inline"></span>
  </div>
</template>

<script>
import CreateContact from './CreateContact';
import ContactList from './ContactList';
import Contact from './Contact';
import MobileDetect from './mixins/MobileDetect';

export default {
  name: 'ContactView',
  mixins: [MobileDetect],
  components: {
    CreateContact,
    ContactList,
    Contact
  },
  props: {
    contacts: {
      type: Array,
      default: []
    }
  },
  data() {
    return {
      localContacts: this.contacts,
      selectedContact: null
    };
  },
  computed: {
    contactSelectedOnMobile() {
      return this.isMobile && !!this.selectedContact;
    }
  },
  methods: {
    handleContactCreated(contact) {
      this.localContacts.push(contact);
    },
    handleContactDeleted(id) {
      this.localContacts = this.localContacts.filter(c => c.id !== id);
    },
    handleContactUpdated(contact) {
      this.localContacts = this.localContacts.map(c => c.id === contact.id ? contact : c);

      if ((this.selectedContact || { id: '' }).id === contact.id) {
        this.selectedContact = contact;
      }
    },
    handleContactSelected(id) {
      this.selectedContact = this.localContacts.find(c => c.id === id);
    },
    resetSelectedContact() {
      this.selectedContact = null;
    }
  }
};
</script>

Update Contact.vue to:

<template>
  <div v-if="isMobile && !contact"></div>
  <div v-else class="card">
    <div class="card-body">
      <h3 class="text-muted text-center" v-if="!contact">No contact selected</h3>
      <div v-else>
        <contact-form
          ref="form"
          button-text="Update contact"
          :initial-data="form"
          @form-submitted="submit" />
        <hr>
        <notes :contact-id="contact.id" />
        <hr>
        <addresses :contact-id="contact.id" />
      </div>
    </div>
  </div>
</template>

<script>
import ContactForm from './ContactForm';
import Notes from './Notes';
import Addresses from './Addresses';
import MobileDetect from './mixins/MobileDetect';

export default {
  name: 'Contact',
  mixins: [MobileDetect],
  components: {
    ContactForm,
    Notes,
    Addresses
  },
  props: {
    contact: {
      type: Object
    }
  },
  data() {
    return {
      form: { name: '', gender: '' }
    };
  },
  watch: {
    contact: {
      deep: true,
      immediate: true,
      handler(c) {
        if (! c) return;

        const { name, gender } = c || { name: '', gender: '' };
        this.form = Object.assign(this.form, { name, gender });

        this.resetForm(this.form);
      }
    }
  },
  methods: {
    submit(form) {
      if (! confirm('Are you sure?')) return;

      axios.put(`/api/contacts/${this.contact.id}`, form)
        .then(({ data: { data } }) => {
          this.$emit('contact-updated', data);

          this.form.name = data.name;
          this.form.gender = data.gender;

          this.resetForm(this.form);
        })
        .catch(({ response }) => {
          alert('Could not create');
        });
    },
    resetForm() {
      if(! this.$refs.form) return;

      this.$refs.form.resetForm(this.form);
    }
  },
}
</script>

You can resize your screen to see it in action. Due to the 5-second interval, the display changes may not be snappy if you switch rapidly change the browser size. You can reduce or increase the interval as needed.

You can view the changes here.

PS: I noticed a bug in our views which you might have noticed too: if you delete the currently viewed contact, the contact remains loaded as the selected contact. Let's rectify that. In ContactView.vue, modify handleContactDeleted():

handleContactDeleted(id) {
      this.localContacts = this.localContacts.filter(c => c.id !== id);

      if (id === (this.selectedContact || { }).id) this.selectedContact = null;
},

You can see the commit here.