Vue Laravel Production Tips

User
@extends('layouts.admin')
@section('plugins.Summernote', true)
@section('plugins.icheckBootstrap', true)
@section('plugins.TempusDominusBs4', true)

{{– Customize layout sections –}}
@section('subtitle', __('Create – Survey'))

{{– Content body: main page content –}}
@section('content_body')

{{ __('Create Surveys') }}

@if($errors->any())

@foreach ($errors->all() as $error)
{{ $error }}
@endforeach

@endif
@if(session()->has('message.level'))

{!! session('message.content') !!}

@endif

1. Survey

2. Pillers

3. Questions

Create Survey

Name

@{{ surveyError }}

{{ __('Allow comments on Survey') }}
{{ __('Allow comments on Pillers') }}
{{ __('Mark as External') }}
Save and Next

{{ __('Add Pillers') }}

{{ __('Piller') }} @{{ index + 1 }}

{{ __('Title') }} (@{{ lang.code.toUpperCase() }})

{{ __('Weight') }}

{{ __('Position') }}

{{ __('Add More Pillers') }}

{{ __('Save and Next') }}

Add Question

Piller

— Select Piller —
@{{ pill.title_en }}

Title (EN)

Title (DE)

Position

Submit

@stop

{{– Push extra scripts –}}
@push('js')

<script>
window.availableLanguages = @json(getLanguages());
    console.log(window.availableLanguages);
</script>
<script>
import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
  document.addEventListener('DOMContentLoaded', () => {
    const app = createApp({
    data() {
      return {
        message: 'Hello Vue!',
        step: 1,
        survey: {
          name: '',
          options: []
        },
        surveyError: '',
        survey_id: null,
        availableLanguages: window.availableLanguages || [],
        pillers: [
      {
        titles: {}, // dynamically assigned
        weight: '',
        position: ''
      }
    ],
        question: {
          piller_id: '',
          title_en: '',
          title_de: '',
          position: ''
        }
      }
    },
    mounted() {
  // Init first piller titles
  this.availableLanguages.forEach(lang => {
    this.pillers[0].titles[lang.code] = '';
  });
},
    methods: {
      goToStep(step) {
    if (step === 2 && !this.survey_id) return;
    if (step === 3 && this.pillers.every(p => !p.id)) return;
    this.step = step;
  },
  addPiller() {
    if (this.pillers.length >= 5) return;
    const newPiller = {
      titles: {},
      weight: '',
      position: ''
    };
    this.availableLanguages.forEach(lang => {
      newPiller.titles[lang.code] = '';
    });
    this.pillers.push(newPiller);
  },
  removePiller(index) {
    this.pillers.splice(index, 1);
  },
      submitSurvey() {
    this.surveyError = '';

    if (!this.survey.name) {
        this.surveyError = 'Survey name is required.';
        return;
    }

    const data = {
        name: this.survey.name,
        allow_comments: this.survey.options.includes('allow_comments') ? 1 : 0,
        allow_comments_on_pillers: this.survey.options.includes('allow_comments_on_pillers') ? 1 : 0,
        mark_as_external: this.survey.options.includes('mark_as_external') ? 1 : 0,
    };

    fetch("{{ route('admin.surveys.store') }}", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "X-CSRF-TOKEN": "{{ csrf_token() }}"
        },
        body: JSON.stringify(data)
    })
    .then(res => res.json().then(json => ({ status: res.status, body: json })))
    .then(({ status, body }) => {
        if (status === 200 && body.success) {
            this.survey_id = body.survey_id;
            Swal.fire({
                icon: 'success',
                title: "{{ __('Survey saved successfully!') }}",
                showConfirmButton: false,
                timer: 1500
            });
            this.step = 2;
        } else {
            Swal.fire({
                icon: 'error',
                title: "{{ __('Validation Error') }}",
                text: body.message || "{{ __('Please check your input.') }}"
            });
        }
    })
    .catch(err => {
        Swal.fire({
            icon: 'error',
            title: "{{ __('Unexpected Error') }}",
            text: "{{ __('Something went wrong while saving the survey.') }}"
        });
    });
},

submitPillers() {
  if (this.pillers.length !== 5) {
    Swal.fire('Validation Error', 'You must add exactly 5 Pillers to continue.', 'warning');
    return;
  }

  // Validate each piller
  for (let [i, piller] of this.pillers.entries()) {
    for (let lang of this.availableLanguages) {
      if (!piller.titles[lang.code]) {
        Swal.fire('Validation Error', `Piller ${i + 1}: Title required for ${lang.title}`, 'error');
        return;
      }
    }
    if (!piller.weight || !piller.position) {
      Swal.fire('Validation Error', `Piller ${i + 1}: Weight and Position are required`, 'error');
      return;
    }
  }

  const data = {
    survey_id: this.survey_id,
    pillers: this.pillers
  };

  fetch("{{ route('admin.pillars.store') }}", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-CSRF-TOKEN": "{{ csrf_token() }}"
    },
    body: JSON.stringify(data)
  })
  .then(res => res.json().then(json => ({ status: res.status, body: json })))
  .then(({ status, body }) => {
    if (status === 200 && body.success) {
      // Save returned piller_ids if needed
      this.pillers = body.pillers;
      Swal.fire({
        icon: 'success',
        title: "{{ __('Pillers saved successfully!') }}",
        showConfirmButton: false,
        timer: 1500
      });
      this.step = 3;
    } else {
      Swal.fire({
        icon: 'error',
        title: "{{ __('Validation Error') }}",
        text: body.message || "{{ __('Please check your input.') }}"
      });
    }
  })
  .catch(() => {
    Swal.fire({
      icon: 'error',
      title: "{{ __('Unexpected Error') }}",
      text: "{{ __('Something went wrong while saving the Pillers.') }}"
    });
  });
},

  submitQuestion() {
    this.message = 'Question added!';
    this.question = { piller_id: '', title_en: '', title_de: '', position: '' };
  }
    }
  });
  app.mount('#surveyApp');
  });
</script>

@endpush
for production

Assistant
For **production**, your Vue 3 multi-step form with Laravel Blade looks well-structured, but here are a few **improvements and best practices** to make it production-ready:

### ✅ **1. Remove Debug Statements**
Remove `console.log(window.availableLanguages);`.

// Remove this in production
console.log(window.availableLanguages);

### ✅ **2. Handle CSRF via Meta Tag (optional but cleaner)**
Instead of embedding the CSRF token directly in JS using `{{ csrf_token() }}`, use Laravel's meta tag and read it from JS:

**Blade (“ section):**

<meta name="csrf-token" content="{{ csrf_token() }}">

**In Vue method:**

const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');

Use it:

headers: {
  'Content-Type': 'application/json',
  'X-CSRF-TOKEN': csrfToken
}

### ✅ **3. Handle Network Errors and Fallbacks**
Make error messages user-friendly and robust:

.catch(error => {
  console.error('Request failed', error);
  Swal.fire({
    icon: 'error',
    title: "Server Error",
    text: "Unable to process your request. Please try again later."
  });
});

### ✅ **4. Add Loader During Submission**
To improve UX, show a loader or disable buttons during async actions.

<button class="btn btn-primary" @click="submitSurvey" :disabled="loading">
  <span v-if="loading" class="spinner-border spinner-border-sm"></span> 
  Save and Next
</button>
data() {
  return {
    loading: false,
    ...
  }
},
methods: {
  async submitSurvey() {
    this.loading = true;
    try {
      ...
    } finally {
      this.loading = false;
    }
  }
}

### ✅ **5. Enable Strict Type Handling (optional but helpful)**
If you have control over your backend and frontend schemas, validate and cast data properly in the Laravel controller (`request()->validate()` and type casts).

### ✅ **6. Defer Non-Essential Scripts**
For performance, make sure you're not loading large plugins unless required. Only include:

@section('plugins.Summernote', false) // set true only if needed

### ✅ **7. Optimize for Accessibility**
Label every field clearly and use `aria-*` where needed for accessibility compliance.

### ✅ **8. Localization Readiness**
You're already using `__()` – make sure all dynamic strings (like validation messages) are translated via Laravel's `lang` files.

### ✅ **9. Vuex or Pinia for Larger State**
If this multi-step wizard grows more complex, you can modularize state using Pinia (preferred for Vue 3).

### ✅ **10. Production Build and Asset Versioning**
Ensure `vite` or `mix` is building the production assets with versioning enabled:

yarn build
# or
npm run build

In Blade:

<script type="module" src="{{ mix('js/app.js') }}"></script>

Let me know if you’d like me to split Vue templates into `.vue` components or refactor logic into Pinia/Vuex for scaling the app!

User
not want to use yarn build because we are not using in full application just in one part
Assistant
Got it — since you're using Vue 3 in just **one section** of your Laravel Blade view (and not as a full SPA or Vite-powered setup), your current usage with CDN via:

<script type="module">
  import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'

…is **totally valid and fine for production**, especially if:

– It's a **small scope** Vue instance (like your 3-step survey wizard),
– You're not relying on a build system (like Vite/Yarn/Webpack),
– And you want to **keep things simple** and easily maintainable.

### ✅ To Summarize for Production Usage (CDN-based Vue)
Here’s what you **should** ensure:

#### 1. ✅ Use the CDN with version locking:

<script type="module">
  import { createApp } from 'https://unpkg.com/vue@3.4.21/dist/vue.esm-browser.js';

🔐 This prevents future unexpected breakage from Vue updates.

#### 2. ✅ Minify if needed
Use `.prod` version if you want a slightly more optimized version:

import { createApp } from 'https://unpkg.com/vue@3.4.21/dist/vue.esm-browser.prod.js';

#### 3. ✅ Remove dev-only code
– Remove all `console.log()`
– Avoid dev alerts or placeholders like `message: 'Hello Vue!'`

#### 4. ✅ Handle missing script fallback (optional)
You can add a “ warning and optionally fallback logic if Vue fails to load — mostly for edge-case users.

#### 5. ✅ Cache Vue via CDN using HTML meta tags (optional but improves perf)
Set CDN caching headers using:

<link rel="preload" as="script" href="https://unpkg.com/vue@3.4.21/dist/vue.esm-browser.prod.js">

If you’re happy with this approach and the Vue section is isolated, there’s **no need for Yarn, npm, or Vite**.

Let me know if you want to optimize any part of the form (e.g., dynamically importing components, making it reusable elsewhere, or integrating Axios via CDN too).

User
@extends('layouts.admin')
@section('plugins.Summernote', true)
@section('plugins.icheckBootstrap', true)
@section('plugins.TempusDominusBs4', true)

{{– Customize layout sections –}}
@section('subtitle', __('Create – Survey'))

{{– Content body: main page content –}}
@section('content_body')

{{ __('Create Surveys') }}

@if($errors->any())

@foreach ($errors->all() as $error)
{{ $error }}
@endforeach

@endif
@if(session()->has('message.level'))

{!! session('message.content') !!}

@endif

1. Survey

2. Pillers

3. Questions

Create Survey

Name

@{{ surveyError }}

{{ __('Allow comments on Survey') }}
{{ __('Allow comments on Pillers') }}
{{ __('Mark as External') }}

Save and Next

{{ __('Add Pillers') }}

{{ __('Piller') }} @{{ index + 1 }}

{{ __('Title') }} (@{{ lang.code.toUpperCase() }})

{{ __('Weight') }}

{{ __('Position') }}

{{ __('Add More Pillers') }}

{{ __('Save and Next') }}

Add Question

Piller

— Select Piller —
@{{ pill.title_en }}

Title (EN)

Title (DE)

Position

Submit

@stop

{{– Push extra scripts –}}
@push('js')

<script>
window.availableLanguages = @json(getLanguages());
    const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
</script>
<script>
import { createApp } from 'https://unpkg.com/vue@3.5.13/dist/vue.esm-browser.prod.js'
  document.addEventListener('DOMContentLoaded', () => {
    const app = createApp({
    data() {
      return {
        message: 'Hello Vue!',
        loading: false,
        step: 1,
        survey: {
          name: '',
          options: []
        },
        surveyError: '',
        survey_id: null,
        availableLanguages: window.availableLanguages || [],
        pillers: [
      {
        titles: {}, // dynamically assigned
        weight: '',
        position: ''
      }
    ],
        question: {
          piller_id: '',
          title_en: '',
          title_de: '',
          position: ''
        }
      }
    },
    mounted() {
  // Init first piller titles
  this.availableLanguages.forEach(lang => {
    this.pillers[0].titles[lang.code] = '';
  });
},
    methods: {
      goToStep(step) {
    if (step === 2 && !this.survey_id) return;
    if (step === 3 && this.pillers.every(p => !p.id)) return;
    this.step = step;
  },
  addPiller() {
    if (this.pillers.length >= 5) return;
    const newPiller = {
      titles: {},
      weight: '',
      position: ''
    };
    this.availableLanguages.forEach(lang => {
      newPiller.titles[lang.code] = '';
    });
    this.pillers.push(newPiller);
  },
  removePiller(index) {
    this.pillers.splice(index, 1);
  },
  async submitSurvey() {
    
    this.surveyError = '';

    if (!this.survey.name) {
        this.surveyError = 'Survey name is required.';
        return;
    }

    const data = {
        name: this.survey.name,
        allow_comments: this.survey.options.includes('allow_comments') ? 1 : 0,
        allow_comments_on_pillers: this.survey.options.includes('allow_comments_on_pillers') ? 1 : 0,
        mark_as_external: this.survey.options.includes('mark_as_external') ? 1 : 0,
    };
    this.loading = true;
    try {
    fetch("{{ route('admin.surveys.store') }}", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "X-CSRF-TOKEN": csrfToken
        },
        body: JSON.stringify(data)
    })
    .then(res => res.json().then(json => ({ status: res.status, body: json })))
    .then(({ status, body }) => {
        if (status === 200 && body.success) {
            this.survey_id = body.survey_id;
            Swal.fire({
                icon: 'success',
                title: "{{ __('Survey saved successfully!') }}",
                showConfirmButton: false,
                timer: 1500
            });
            this.step = 2;
        } else {
            Swal.fire({
                icon: 'error',
                title: "{{ __('Validation Error') }}",
                text: body.message || "{{ __('Please check your input.') }}"
            });
        }
    })
    .catch(err => {
        Swal.fire({
            icon: 'error',
            title: "{{ __('Unexpected Error') }}",
            text: "{{ __('Something went wrong while saving the survey.') }}"
        });
    });
    } catch (error) {
      Swal.fire({
        icon: 'error',
        title: "{{ __('Unexpected Error') }}",
        text: "{{ __('Something went wrong while saving the survey.') }}"
      });
    } finally {
      this.loading = false;
    }
},

submitPillers() {
  if (this.pillers.length !== 5) {
    Swal.fire('Validation Error', 'You must add exactly 5 Pillers to continue.', 'warning');
    return;
  }

  // Validate each piller
  for (let [i, piller] of this.pillers.entries()) {
    for (let lang of this.availableLanguages) {
      if (!piller.titles[lang.code]) {
        Swal.fire('Validation Error', `Piller ${i + 1}: Title required for ${lang.title}`, 'error');
        return;
      }
    }
    if (!piller.weight || !piller.position) {
      Swal.fire('Validation Error', `Piller ${i + 1}: Weight and Position are required`, 'error');
      return;
    }
  }

  const data = {
    survey_id: this.survey_id,
    pillers: this.pillers
  };
  this.loading = true;
  try {
  fetch("{{ route('admin.pillars.store') }}", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-CSRF-TOKEN": csrfToken
    },
    body: JSON.stringify(data)
  })
  .then(res => res.json().then(json => ({ status: res.status, body: json })))
  .then(({ status, body }) => {
    if (status === 200 && body.success) {
      // Save returned piller_ids if needed
      this.pillers = body.pillers;
      Swal.fire({
        icon: 'success',
        title: "{{ __('Pillers saved successfully!') }}",
        showConfirmButton: false,
        timer: 1500
      });
      this.step = 3;
    } else {
      Swal.fire({
        icon: 'error',
        title: "{{ __('Validation Error') }}",
        text: body.message || "{{ __('Please check your input.') }}"
      });
    }
  })
  .catch(() => {
    Swal.fire({
      icon: 'error',
      title: "{{ __('Unexpected Error') }}",
      text: "{{ __('Something went wrong while saving the Pillers.') }}"
    });
  });
  } catch (error) {
    Swal.fire({
      icon: 'error',
      title: "{{ __('Unexpected Error') }}",
      text: "{{ __('Something went wrong while saving the Pillers.') }}"
    });
  } finally {
    this.loading = false;
  }
},

  submitQuestion() {
    this.message = 'Question added!';
    this.question = { piller_id: '', title_en: '', title_de: '', position: '' };
  }
    }
  });
  app.mount('#surveyApp');
  });
</script>

@endpush
some times it takes time to fully effect vue js on page
add loading till thrn

Assistant
To handle the delay before Vue is fully mounted and interactive, you can display a **page loader/spinner** that hides **after `app.mount()` completes**. Here's how to do it cleanly:

### ✅ Step 1: Add a Loading Overlay in Blade

Put this just **above** your `#surveyApp` div:

<div id="vue-loading" class="position-fixed w-100 h-100 d-flex justify-content-center align-items-center bg-white" style="z-index: 9999;">
  <div class="spinner-border text-primary" role="status">
    <span class="sr-only">Loading...</span>
  </div>
</div>

### ✅ Step 2: CSS (Optional for smooth fade-out)
Add this somewhere in your styles (or inline if needed):

<style>
  #vue-loading.fade-out {
    opacity: 0;
    transition: opacity 0.4s ease;
    pointer-events: none;
  }
</style>

### ✅ Step 3: Hide Loader After Vue is Mounted

Inside your `DOMContentLoaded` callback (right **after** `app.mount('#surveyApp')`):

app.mount('#surveyApp');

// Hide loading spinner after mount
const loadingDiv = document.getElementById('vue-loading');
if (loadingDiv) {
  loadingDiv.classList.add('fade-out');
  setTimeout(() => loadingDiv.remove(), 500); // fully remove from DOM after fade
}

### ✅ Final Blade Snippet Reference

Just before `#surveyApp`:

<div id="vue-loading" class="position-fixed w-100 h-100 d-flex justify-content-center align-items-center bg-white" style="z-index: 9999;">
  <div class="spinner-border text-primary" role="status">
    <span class="sr-only">Loading...</span>
  </div>
</div>
<div id="surveyApp" class="container-fluid mt-4">
  <!-- Vue content -->
</div>

This ensures users see a clean loading state until Vue is fully reactive and ready. Let me know if you'd like to make the loader full-page or show a logo or message too.

User
display loading in center
Assistant
To display the loader **perfectly centered on the screen**, you're almost there — just a small update to your loader's HTML and styling.

### ✅ Use This Exact Loader Markup in Blade (Above `#surveyApp`)

<div id="vue-loading"
     style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: white; z-index: 9999;"
     class="d-flex justify-content-center align-items-center">
  <div class="text-center">
    <div class="spinner-border text-primary" role="status" style="width: 3rem; height: 3rem;">
      <span class="sr-only">Loading...</span>
    </div>
    <div class="mt-2">Loading app...</div>
  </div>
</div>

### ✅ Add This Optional CSS (for smooth fade-out):

<style>
  #vue-loading.fade-out {
    opacity: 0;
    transition: opacity 0.4s ease;
    pointer-events: none;
  }
</style>

### ✅ Add Fade-Out Logic After Vue Mounts

Place this right after `app.mount('#surveyApp')` in your JS:

app.mount('#surveyApp');

const loadingDiv = document.getElementById('vue-loading');
if (loadingDiv) {
  loadingDiv.classList.add('fade-out');
  setTimeout(() => loadingDiv.remove(), 500); // Remove from DOM after fade
}

This will ensure:
– Full-screen white background loader
– Centered spinning loader and optional message
– Clean fade-out once Vue mounts

Let me know if you want to show a custom image/logo or animate the loading message.

Leave a Comment

Your email address will not be published. Required fields are marked *