avatarShaun Thornburgh

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

32550

Abstract

<span class="hljs-name">th</span> <span class="hljs-attr">scope</span>=<span class="hljs-string">"col"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0 w-1/2"</span>></span>Name<span class="hljs-tag"></<span class="hljs-name">th</span>></span> <span class="hljs-tag"><<span class="hljs-name">th</span> <span class="hljs-attr">scope</span>=<span class="hljs-string">"col"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"px-3 py-3.5 text-left text-sm font-semibold text-gray-900"</span>></span>Priority<span class="hljs-tag"></<span class="hljs-name">th</span>></span> <span class="hljs-tag"><<span class="hljs-name">th</span> <span class="hljs-attr">scope</span>=<span class="hljs-string">"col"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"px-3 py-3.5 text-left text-sm font-semibold text-gray-900"</span>></span>Status<span class="hljs-tag"></<span class="hljs-name">th</span>></span> <span class="hljs-tag"><<span class="hljs-name">th</span> <span class="hljs-attr">scope</span>=<span class="hljs-string">"col"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"px-3 py-3.5 text-left text-sm font-semibold text-gray-900"</span>></span>Due By<span class="hljs-tag"></<span class="hljs-name">th</span>></span> <span class="hljs-tag"><<span class="hljs-name">th</span> <span class="hljs-attr">scope</span>=<span class="hljs-string">"col"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"relative py-3.5 pl-3 pr-4 sm:pr-0"</span>></span> <span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"sr-only"</span>></span>Edit<span class="hljs-tag"></<span class="hljs-name">span</span>></span> <span class="hljs-tag"></<span class="hljs-name">th</span>></span> <span class="hljs-tag"><<span class="hljs-name">th</span> <span class="hljs-attr">scope</span>=<span class="hljs-string">"col"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"relative py-3.5 pl-3 pr-4 sm:pr-0"</span>></span> <span class="hljs-tag"><<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"sr-only"</span>></span>Delete<span class="hljs-tag"></<span class="hljs-name">span</span>></span> <span class="hljs-tag"></<span class="hljs-name">th</span>></span> <span class="hljs-tag"></<span class="hljs-name">tr</span>></span> <span class="hljs-tag"></<span class="hljs-name">thead</span>></span> <span class="hljs-tag"><<span class="hljs-name">tbody</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"divide-y divide-gray-200"</span>></span> <span class="hljs-tag"><<span class="hljs-name">table-row</span> <span class="hljs-attr">v-for</span>=<span class="hljs-string">"task in tasks"</span> <span class="hljs-attr">:key</span>=<span class="hljs-string">"task.id"</span> <span class="hljs-attr">:task</span>=<span class="hljs-string">"task"</span> @<span class="hljs-attr">editClicked</span>=<span class="hljs-string">"openFormModal(task)"</span> @<span class="hljs-attr">deleteClicked</span>=<span class="hljs-string">"openDeleteConfirmationModal(task)"</span> /></span> <span class="hljs-tag"></<span class="hljs-name">tbody</span>></span> <span class="hljs-tag"></<span class="hljs-name">table</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"><<span class="hljs-name">Modal</span> <span class="hljs-attr">:show</span>=<span class="hljs-string">"showFormModal"</span>></span> <span class="hljs-tag"><<span class="hljs-name">TaskForm</span> <span class="hljs-attr">:task</span>=<span class="hljs-string">"selectedTask"</span> @<span class="hljs-attr">close</span>=<span class="hljs-string">"closeFormModal"</span>/></span> <span class="hljs-tag"></<span class="hljs-name">Modal</span>></span> <span class="hljs-tag"><<span class="hljs-name">Modal</span> <span class="hljs-attr">:show</span>=<span class="hljs-string">"showDeleteConfirmationModal"</span> @<span class="hljs-attr">close</span>=<span class="hljs-string">"closeDeleteConfirmationModal"</span>></span> <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"p-6"</span>></span> <span class="hljs-tag"><<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-lg font-medium text-gray-900"</span>></span> Are you sure you want to delete this task? <span class="hljs-tag"></<span class="hljs-name">h2</span>></span>

            <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-1 text-sm text-gray-600"</span>&gt;</span>
                Once your task is deleted, all of it's data will be permanently deleted. Please
                click the delete button to confirm.
            <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>

            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-6 flex justify-end"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">SecondaryButton</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"closeDeleteConfirmationModal"</span>&gt;</span> Cancel <span class="hljs-tag">&lt;/<span class="hljs-name">SecondaryButton</span>&gt;</span>

                <span class="hljs-tag">&lt;<span class="hljs-name">DangerButton</span>
                    <span class="hljs-attr">class</span>=<span class="hljs-string">"ml-3"</span>
                    <span class="hljs-attr">:class</span>=<span class="hljs-string">"{ 'opacity-25': form.processing }"</span>
                    <span class="hljs-attr">:disabled</span>=<span class="hljs-string">"form.processing"</span>
                    @<span class="hljs-attr">click</span>=<span class="hljs-string">"deleteTask"</span>
                &gt;</span>
                    Delete Task
                <span class="hljs-tag">&lt;/<span class="hljs-name">DangerButton</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Modal</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">AuthenticatedLayout</span>&gt;</span>

<span class="hljs-tag"></<span class="hljs-name">template</span>></span></pre></div><p id="ca35">We are using a number of components that are included with the Laravel Breeze starter kit. However I have created a component for creating and editing a task called <code>TaskForm.vue</code>:</p><div id="798e"><pre><span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">setup</span>></span><span class="language-javascript"> <span class="hljs-keyword">import</span> <span class="hljs-title class_">InputError</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'@/Components/InputError.vue'</span>; <span class="hljs-keyword">import</span> <span class="hljs-title class_">InputLabel</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'@/Components/InputLabel.vue'</span>; <span class="hljs-keyword">import</span> <span class="hljs-title class_">SecondaryButton</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'@/Components/SecondaryButton.vue'</span>; <span class="hljs-keyword">import</span> <span class="hljs-title class_">TextInput</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'@/Components/TextInput.vue'</span>; <span class="hljs-keyword">import</span> {useForm} <span class="hljs-keyword">from</span> <span class="hljs-string">"@inertiajs/vue3"</span>; <span class="hljs-keyword">import</span> <span class="hljs-title class_">PrimaryButton</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'@/Components/PrimaryButton.vue'</span>; <span class="hljs-keyword">import</span> <span class="hljs-title class_">VueTailwindDatepicker</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'vue-tailwind-datepicker'</span>;

<span class="hljs-keyword">const</span> props = <span class="hljs-title function_">defineProps</span>({ <span class="hljs-attr">task</span>: <span class="hljs-title class_">Object</span> })

<span class="hljs-keyword">const</span> form = <span class="hljs-title function_">useForm</span>({ <span class="hljs-attr">id</span>: props.<span class="hljs-property">task</span>.<span class="hljs-property">id</span>, <span class="hljs-attr">name</span>: props.<span class="hljs-property">task</span>.<span class="hljs-property">name</span> || <span class="hljs-string">''</span>, <span class="hljs-attr">priority</span>: props.<span class="hljs-property">task</span>.<span class="hljs-property">priority</span> || <span class="hljs-string">''</span>, <span class="hljs-attr">status</span>: props.<span class="hljs-property">task</span>.<span class="hljs-property">status</span> || <span class="hljs-string">''</span>, <span class="hljs-attr">due_by</span>: props.<span class="hljs-property">task</span>.<span class="hljs-property">due_by</span> || <span class="hljs-string">''</span> })

<span class="hljs-keyword">const</span> emit = <span class="hljs-title function_">defineEmits</span>([<span class="hljs-string">'close'</span>]);

<span class="hljs-keyword">const</span> <span class="hljs-title function_">closeModal</span> = (<span class="hljs-params"></span>) => { form.<span class="hljs-title function_">reset</span>(); <span class="hljs-title function_">emit</span>(<span class="hljs-string">'close'</span>); }

<span class="hljs-keyword">const</span> <span class="hljs-title function_">store</span> = (<span class="hljs-params"></span>) => { form.<span class="hljs-title function_">post</span>(<span class="hljs-title function_">route</span>(<span class="hljs-string">'tasks.store'</span>), { <span class="hljs-attr">onSuccess</span>: <span class="hljs-function">() =></span> <span class="hljs-title function_">closeModal</span>(), }); }

<span class="hljs-keyword">const</span> <span class="hljs-title function_">update</span> = (<span class="hljs-params"></span>) => { form.<span class="hljs-title function_">put</span>(<span class="hljs-title function_">route</span>(<span class="hljs-string">'tasks.update'</span>, {<span class="hljs-string">'task'</span>: form.<span class="hljs-property">id</span>}), { <span class="hljs-attr">onSuccess</span>: <span class="hljs-function">() =></span> <span class="hljs-title function_">closeModal</span>(), }); }

<span class="hljs-keyword">const</span> <span class="hljs-title function_">editing</span> = (<span class="hljs-params"></span>) => { <span class="hljs-keyword">return</span> <span class="hljs-title class_">Object</span>.<span class="hljs-title function_">keys</span>(props.<span class="hljs-property">task</span>).<span class="hljs-property">length</span> }

</span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>

<span class="hljs-tag"><<span class="hljs-name">template</span>></span> <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"p-6"</span>></span> <span class="hljs-tag"><<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-lg font-medium text-gray-900"</span>></span> {{ editing() ? 'Edit' : 'Create '}} a task. <span class="hljs-tag"></<span class="hljs-name">h2</span>></span>

    <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-1 text-sm text-gray-600"</span>&gt;</span>
        Enter the details below for your task and click enter to create.
    <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-6"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">InputLabel</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"name"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"Name"</span>  /&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">TextInput</span>
            <span class="hljs-attr">id</span>=<span class="hljs-string">"name"</span>
            <span class="hljs-attr">ref</span>=<span class="hljs-string">"nameInput"</span>
            <span class="hljs-attr">v-model</span>=<span class="hljs-string">"form.name"</span>
            <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>
            <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-1 block w-3/4"</span>
            <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Name"</span>
        /&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">InputError</span> <span class="hljs-attr">:message</span>=<span class="hljs-string">"form.errors.name"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-2"</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-6"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">InputLabel</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"status"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"Status"</span>  /&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">select</span>
            <span class="hljs-attr">class</span>=<span class="hljs-string">"border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"</span>
            <span class="hljs-attr">id</span>=<span class="hljs-string">"status"</span>
            <span class="hljs-attr">name</span>=<span class="hljs-string">"status"</span>
            <span class="hljs-attr">v-model</span>=<span class="hljs-string">"form.status"</span>
        &gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span>&gt;</span>Not started<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span>&gt;</span>In progress<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span>&gt;</span>Completed<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">select</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">InputError</span> <span class="hljs-attr">:message</span>=<span class="hljs-string">"form.errors.status"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-2"</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-6"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">InputLabel</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"priority"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"Priority"</span>  /&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">select</span>
            <span class="hljs-attr">class</span>=<span class="hljs-string">"border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"</span>
            <span class="hljs-attr">name</span>=<span class="hljs-string">"status"</span>
            <span class="hljs-attr">id</span>=<span class="hljs-string">"status"</span>
            <span class="hljs-attr">v-model</span>=<span class="hljs-string">"form.priority"</span>
        &gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span>&gt;</span>Low<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span>&gt;</span>Medium<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">option</span>&gt;</span>High<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">select</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">InputError</span> <span class="hljs-attr">:message</span>=<span class="hljs-string">"form.errors.priority"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-2"</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-6"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">InputLabel</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"due_by"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"Due By"</span> /&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">vue-tailwind-datepicker</span>
            <span class="hljs-attr">as-single</span>
            <span class="hljs-attr">v-model</span>=<span class="hljs-string">"form.due_by"</span>
            <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Due by"</span>
            <span class="hljs-attr">no-input</span>
            <span class="hljs-attr">name</span>=<span class="hljs-string">"due_by"</span>
        /&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">InputError</span> <span class="hljs-attr">:message</span>=<span class="hljs-string">"form.errors.due_by"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-2"</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-6 flex justify-end"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">SecondaryButton</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"closeModal"</span>&gt;</span> Cancel <span class="hljs-tag">&lt;/<span class="hljs-name">SecondaryButton</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">PrimaryButton</span>
            <span class="hljs-attr">v-if</span>=<span class="hljs-string">"editing()"</span>
            <span class="hljs-attr">class</span>=<span class="hljs-string">"ml-3"</span>
            <span class="hljs-attr">:class</span>=<span class="hljs-string">"{ 'opacity-25': form.processing }"</span>
            <span class="hljs-attr">:disabled</span>=<span class="hljs-string">"form.processing"</span>
            @<span class="hljs-attr">click</span>=<span class="hljs-string">"update"</span>
        &gt;</span>
            Save Changes
        <span class="hljs-tag">&lt;/<span class="hljs-name">PrimaryButton</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">PrimaryButton</span>
            <span class="hljs-attr">v-else</span>
            <span class="hljs-attr">class</span>=<span class="hljs-string">"ml-3"</span>
            <span class="hljs-attr">:class</span>=<span class="hljs-string">"{ 'opacity-25': form.processing }"</span>
            <span class="hljs-attr">:disabled</span>=<span class="hljs-string">"form.processing"</span>
            @<span class="hljs-attr">click</span>=<span class="hljs-string">"store"</span>
        &gt;</span>
            Create Task
        <span class="hljs-tag">&lt;/<span class="hljs-name">PrimaryButton</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

<span class="hljs-tag"></<span class="hljs-name">template</span>></span></pre></div><p id="8b29">I have also created a compenent for each row in the table of tasks called <code>TaskRow.vue.</code></p><div id="4575"><pre><span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">setup</span>></span><span class="language-javascript"> <span class="hljs-keyword">import</span> <span class="hljs-title class_">SecondaryButton</span> <span class="hljs-keyword">from</span> <span class="hljs-string">"@/Components/SecondaryButton.vue"</span>; <span class="hljs-keyword">import</span> <span class="hljs-title class_">Red</span> <span class="hljs-keyword">from</span> <span class="hljs-string">"@/Components/Badges/Red.vue"</span>; <span class="hljs-keyword">import</span> <span class="hljs-title class_">Yellow</span> <span class="hljs-keyword">from</span> <span class="hljs-string">"@/Components/Badges/Yellow.vue"</span>; <span class="hljs-keyword">import</span> <span class="hljs-title class_">Green</span> <span class="hljs-keyword">from</span> <span class="hljs-string">"@/Components/Badges/Green.vue"</span>; <span class="hljs-keyword">import</span> <span class="hljs-title class_">DangerButton</span> <span class="hljs-keyword">from</span> <span class="hljs-string">"@/Components/DangerButton.vue"</span>;

<span class="hljs-keyword">const</span> props = <span class="hljs-title function_">defineProps</span>({ <span class="hljs-attr">task</span>: <span class="hljs-title class_">Object</span> })

<span class="hljs-keyword">const</span> emit = <span class="hljs-title function_">defineEmits</span>([<span class="hljs-string">'editClicked'</span>, <span class="hljs-string">'deleteClicked'</span>])

<span class="hljs-keyword">const</span> <span class="hljs-title function_">handleEditClick</span> = (<span class="hljs-params"></span>) => { <span class="hljs-title function_">emit</span>(<span class="hljs-string">'editClicked'</span>, props.<span class="hljs-property">task</span>); }

<span class="hljs-keyword">const</span> <span class="hljs-title function_">handleDeleteClick</span> = (<span class="hljs-params"></span>) => { <span class="hljs-title function_">emit</span>(<span class="hljs-string">'deleteClicked'</span>, props.<span class="hljs-property">task</span>); } </span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>

<span class="hljs-tag"><<span class="hljs-name">template</span>></span> <span class="hljs-tag"><<span class="hljs-name">tr</span>></span> <span class="hljs-tag"><<span class="hljs-name">td</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0"</span>></span>{{ props.task.name }}<span class="hljs-tag"></<span class="hljs-name">td</span>></span> <span class="hljs-tag"><<span class="hljs-name">td</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"whitespace-nowrap px-3 py-5 text-sm text-gray-500"</span>></span> <span class="hljs-tag"><<span class="hljs-name">red</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"props.task.priority === 'High'"</span>></span>{{ props.task.priority }}<span class="hljs-tag"></<span class="hljs-name">red</span>></span> <span class="hljs-tag"><<span class="hljs-name">yellow</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"props.task.priority === 'Medium'"</span>></span>{{ props.task.priority }}<span class="hljs-tag"></<span class="hljs-name">yellow</span>></span> <span class="hljs-tag"><<span class="hljs-name">green</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"props.task.priority === 'Low'"</span>></span>{{ props.task.priority }}<span class="hljs-tag"></<span class="hljs-name">green</span>></span> <span class="hljs-tag"></<span class="hljs-name">td</span>></span> <span class="hljs-tag"><<span class="hljs-name">td</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"whitespace-nowrap px-3 py-5 text-sm text-gray-500"</span>></span> <span class="hljs-tag"><<span class="hljs-name">red</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"props.task.status === 'Not started'"</span>></span>{{ props.task.status }}<span class="hljs-tag"></<span class="hljs-name">red</span>></span> <span class="hljs-tag"><<span class="hljs-name">yellow</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"props.task.status === 'In progress'"</span>></span>{{ props.task.status }}<span class="hljs-tag"></<span class="hljs-name">yellow</span>></span> <span class="hljs-tag"><<span class="hljs-name">green</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"props.task.status === 'Completed'"</span>></span>{{ props.task.status }}<span class="hljs-tag"></<span class="hljs-name">green</span>></span> <span class="hljs-tag"></<span class="hljs-name">td</span>></span> <span class="hljs-tag"><<span class="hljs-name">td</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"whitespace-nowrap px-3 py-4 text-sm text-gray-500"</span>></span>{{ props.task.due_by }}<span class="hljs-tag"></<span class="hljs-name">td</span>></span> <span class="hljs-tag"><<span class="hljs-name">td</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0"</span>></span> <span class="hljs-tag"><<span class="hljs-name">SecondaryButton</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"handleEditClick"</span>></span>Edit<span class="hljs-tag"></<span class="hljs-name">SecondaryButton</span>></span> <span class="hljs-tag"></<span class="hljs-name">td</span>></span> <span class="hljs-tag"><<span class="hljs-name">td</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0"</span>></span> <span class="hljs-tag"><<span class="hljs-name">DangerButton</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"handleDeleteClick"</span>></span>Delete<span class="hljs-tag"></<span class="hljs-name">DangerButton</span>></span> <span class="hljs-tag"></<span class="hljs-name">td</span>></span> <span class="hljs-tag"></<span class="hljs-name">tr</span>></span> <span class="hljs-tag"></<span class="hljs-name">template</span>></span></pre></div><p id="7652">Let’s create some test data. Create a factory class so we can populdate the tasks table:</p><div id="a470"><pre>sail artisan make:factory TaskFactory</pre></div><div id="50d7"><pre><span class="hljs-meta"><?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title class_">Database</span><span class="hljs-title class_">Factories</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span><span class="hljs-title">Models</span><span class="hljs-title">Task</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">App</span><span class="hljs-title">Models</span><span class="hljs-title">User</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span><span class="hljs-title">Database</span><span class="hljs-title">Eloquent</span><span class="hljs-title">Factories</span><span class="hljs-title">Factory</span>;

<span class="hljs-comment">/**

  • <span class="hljs-doctag">@extends</span> Factory<Task> /</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TaskFactory</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Factory</span> </span>{ <span class="hljs-comment">/*
    • Define the model's default state.
    • <span class="hljs-doctag">@return</span> array<string, mixed> */</span> <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">definition</span>(<span class="hljs-params"></span>): <span class="hljs-title">array</span> </span>{ <span class="hljs-keyword">return</span> [ <span class="hljs-string">'name'</span> => <span class="hljs-title function_ invoke__">fake</span>()->sentence, <span class="hljs-string">'priority'</span> => <span class="hljs-title class_">Task</span>::<span class="hljs-variable constant_">PRIORITIES</span>[<span class="hljs-title function_ invoke__">array_rand</span>(<span class="hljs-title class_">Task</span>::<span class="hljs-variable constant_">PRIORITIES</span>)], <span class="hljs-string">'status'</span> => <span class="hljs-title class_">Task</span>::<span class="hljs-variable constant_">STATUSES</span>[<span class="hljs-title function_ invoke__">array_rand</span>(<span class="hljs-title class_">Task</span>::<span class="hljs-variable constant_">STATUSES</span>)], <span class="hljs-string">'user_id'</span> => <span class="hljs-title class_">User</span>::<span class="hljs-title function_ invoke__">factory</span>(), <span class="hljs-string">'due_by'</span> => <span class="hljs-title function_ invoke__">fake</span>()-><span class="hljs-title function_ invoke__">dateTimeBetween</span>(<span class="hljs-string">'+0 days'</span>, <span class="hlj

Options

s-string">'+2 weeks'</span>)-><span class="hljs-title function_ invoke__">format</span>(<span class="hljs-string">'Y-m-d'</span>) ]; } }</pre></div><p id="da94">Head over to <code>Tinker</code> and run the following:</p><figure id="c0a4"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*DmrfXr-yiOi2GDY7bfkznQ.png"><figcaption></figcaption></figure><p id="1b71">This will create a user with 20 tasks assigned to them. Log in with this user and this is what we see:</p><figure id="8e9c"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*NAdcXXghjn3j5hDfNPBtCg.png"><figcaption>Task list</figcaption></figure><p id="0216">And this is the Task form:</p><figure id="e2e9"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*SZWW94s7yAsk-IksdVYvgQ.png"><figcaption></figcaption></figure><p id="6191">You will notice I have used a date picker for the due_by field, full details for this package can be found here <a href="https://vue-tailwind-datepicker.com/">https://vue-tailwind-datepicker.com/</a></p><h2 id="9ac4">Step 5: Write some Tests</h2><p id="1070">Let’s write some tests to ensure our code is functioning as expetcted. Test-Driven Development (TDD) is a software development practice that emphasises writing tests before writing the actual code for a software component or feature. The core idea is to shift the focus from just writing code to writing testable, maintainable, and clean code.</p><div id="f63b"><pre><span class="hljs-meta"><?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title class_">Tests</span><span class="hljs-title class_">Feature</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span><span class="hljs-title">Models</span><span class="hljs-title">Task</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">App</span><span class="hljs-title">Models</span><span class="hljs-title">User</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Database</span><span class="hljs-title">Factories</span><span class="hljs-title">TaskFactory</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span><span class="hljs-title">Foundation</span><span class="hljs-title">Testing</span><span class="hljs-title">RefreshDatabase</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span><span class="hljs-title">Foundation</span><span class="hljs-title">Testing</span><span class="hljs-title">WithFaker</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">JsonException</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Tests</span><span class="hljs-title">TestCase</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TaskTest</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">TestCase</span> </span>{ <span class="hljs-keyword">use</span> <span class="hljs-title">RefreshDatabase</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">WithFaker</span>;

<span class="hljs-comment">/** <span class="hljs-doctag">@test</span> */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">a_user_can_view_their_tasks</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
</span>{
    <span class="hljs-variable">$user</span> = <span class="hljs-title class_">User</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>();

    <span class="hljs-variable">$task</span> = <span class="hljs-title class_">Task</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>([<span class="hljs-string">'user_id'</span> =&gt; <span class="hljs-variable">$user</span>-&gt;id]);

    <span class="hljs-variable language_">$this</span>
        -&gt;<span class="hljs-title function_ invoke__">actingAs</span>(<span class="hljs-variable">$user</span>)
        -&gt;<span class="hljs-title function_ invoke__">get</span>(<span class="hljs-title function_ invoke__">route</span>(<span class="hljs-string">'tasks.index'</span>))
        -&gt;<span class="hljs-title function_ invoke__">assertSee</span>(<span class="hljs-variable">$task</span>-&gt;name);
}

<span class="hljs-comment">/** <span class="hljs-doctag">@test</span>
 * <span class="hljs-doctag">@throws</span> JsonException
 */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">a_user_can_create_a_task</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
</span>{
    <span class="hljs-variable">$user</span> = <span class="hljs-title class_">User</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>();

    <span class="hljs-variable">$task</span> = <span class="hljs-title class_">Task</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">raw</span>([<span class="hljs-string">'user_id'</span> =&gt; <span class="hljs-variable">$user</span>-&gt;id]);

    <span class="hljs-variable">$response</span> = <span class="hljs-variable language_">$this</span>
        -&gt;<span class="hljs-title function_ invoke__">actingAs</span>(<span class="hljs-variable">$user</span>)
        -&gt;<span class="hljs-title function_ invoke__">post</span>(<span class="hljs-title function_ invoke__">route</span>(<span class="hljs-string">'tasks.store'</span>), <span class="hljs-variable">$task</span>);

    <span class="hljs-variable">$response</span>
        -&gt;<span class="hljs-title function_ invoke__">assertSessionHasNoErrors</span>()
        -&gt;<span class="hljs-title function_ invoke__">assertRedirect</span>(<span class="hljs-title function_ invoke__">route</span>(<span class="hljs-string">'tasks.index'</span>));

    <span class="hljs-variable language_">$this</span>-&gt;<span class="hljs-title function_ invoke__">assertDatabaseHas</span>(<span class="hljs-string">'tasks'</span>, <span class="hljs-variable">$task</span>);
}

<span class="hljs-comment">/** <span class="hljs-doctag">@test</span> */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">a_user_can_update_a_task</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
</span>{
    <span class="hljs-variable">$user</span> = <span class="hljs-title class_">User</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>();

    <span class="hljs-variable">$task</span> = <span class="hljs-title class_">Task</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>([<span class="hljs-string">'user_id'</span> =&gt; <span class="hljs-variable">$user</span>-&gt;id]);

    <span class="hljs-variable">$response</span> = <span class="hljs-variable language_">$this</span>
        -&gt;<span class="hljs-title function_ invoke__">actingAs</span>(<span class="hljs-variable">$user</span>)
        -&gt;<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-title function_ invoke__">route</span>(<span class="hljs-string">'tasks.update'</span>, <span class="hljs-variable">$task</span>-&gt;id), [
            <span class="hljs-string">'name'</span> =&gt; <span class="hljs-variable">$updatedName</span> = <span class="hljs-title function_ invoke__">fake</span>()-&gt;sentence,
            <span class="hljs-string">'priority'</span> =&gt; <span class="hljs-variable">$updatedPriority</span> = <span class="hljs-title class_">Task</span>::<span class="hljs-variable constant_">PRIORITIES</span>[<span class="hljs-title function_ invoke__">array_rand</span>(<span class="hljs-title class_">Task</span>::<span class="hljs-variable constant_">PRIORITIES</span>)],
            <span class="hljs-string">'status'</span> =&gt; <span class="hljs-variable">$updatedStatus</span> = <span class="hljs-title class_">Task</span>::<span class="hljs-variable constant_">STATUSES</span>[<span class="hljs-title function_ invoke__">array_rand</span>(<span class="hljs-title class_">Task</span>::<span class="hljs-variable constant_">STATUSES</span>)],
        ]);

    <span class="hljs-variable">$response</span>
        -&gt;<span class="hljs-title function_ invoke__">assertSessionHasNoErrors</span>()
        -&gt;<span class="hljs-title function_ invoke__">assertRedirect</span>(<span class="hljs-title function_ invoke__">route</span>(<span class="hljs-string">'tasks.index'</span>));

    <span class="hljs-variable language_">$this</span>-&gt;<span class="hljs-title function_ invoke__">assertDatabaseHas</span>(<span class="hljs-string">'tasks'</span>, [
        <span class="hljs-string">'id'</span> =&gt; <span class="hljs-variable">$task</span>-&gt;id,
        <span class="hljs-string">'name'</span> =&gt; <span class="hljs-variable">$updatedName</span>,
        <span class="hljs-string">'priority'</span> =&gt; <span class="hljs-variable">$updatedPriority</span>,
        <span class="hljs-string">'status'</span> =&gt; <span class="hljs-variable">$updatedStatus</span>,
    ]);
}

<span class="hljs-comment">/** <span class="hljs-doctag">@test</span> */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">a_user_can_delete_a_task</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
</span>{
    <span class="hljs-variable">$user</span> = <span class="hljs-title class_">User</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>();

    <span class="hljs-variable">$task</span> = <span class="hljs-title class_">Task</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>([<span class="hljs-string">'user_id'</span> =&gt; <span class="hljs-variable">$user</span>-&gt;id]);

    <span class="hljs-variable">$response</span> = <span class="hljs-variable language_">$this</span>
        -&gt;<span class="hljs-title function_ invoke__">actingAs</span>(<span class="hljs-variable">$user</span>)
        -&gt;<span class="hljs-title function_ invoke__">delete</span>(<span class="hljs-title function_ invoke__">route</span>(<span class="hljs-string">'tasks.destroy'</span>, <span class="hljs-variable">$task</span>-&gt;id));

    <span class="hljs-variable">$response</span>
        -&gt;<span class="hljs-title function_ invoke__">assertSessionHasNoErrors</span>()
        -&gt;<span class="hljs-title function_ invoke__">assertRedirect</span>(<span class="hljs-title function_ invoke__">route</span>(<span class="hljs-string">'tasks.index'</span>));

    <span class="hljs-variable language_">$this</span>-&gt;<span class="hljs-title function_ invoke__">assertDatabaseMissing</span>(<span class="hljs-string">'tasks'</span>, <span class="hljs-variable">$task</span>-&gt;<span class="hljs-title function_ invoke__">toArray</span>());
}

<span class="hljs-comment">/** <span class="hljs-doctag">@test</span> */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">creating_a_task_requires_a_name</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
</span>{
    <span class="hljs-variable">$user</span> = <span class="hljs-title class_">User</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>();

    <span class="hljs-variable">$task</span> = <span class="hljs-title class_">Task</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">raw</span>([
        <span class="hljs-string">'user_id'</span> =&gt; <span class="hljs-variable">$user</span>-&gt;id,
        <span class="hljs-string">'name'</span> =&gt; <span class="hljs-literal">null</span>
    ]);

    <span class="hljs-variable">$response</span> = <span class="hljs-variable language_">$this</span>
        -&gt;<span class="hljs-title function_ invoke__">actingAs</span>(<span class="hljs-variable">$user</span>)
        -&gt;<span class="hljs-title function_ invoke__">post</span>(<span class="hljs-title function_ invoke__">route</span>(<span class="hljs-string">'tasks.store'</span>), <span class="hljs-variable">$task</span>);

    <span class="hljs-variable">$response</span>
        -&gt;<span class="hljs-title function_ invoke__">assertSessionHasErrors</span>(<span class="hljs-string">'name'</span>);

    <span class="hljs-variable language_">$this</span>-&gt;<span class="hljs-title function_ invoke__">assertDatabaseMissing</span>(<span class="hljs-string">'tasks'</span>, <span class="hljs-variable">$task</span>);
}

<span class="hljs-comment">/** <span class="hljs-doctag">@test</span> */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">updating_a_task_requires_a_name</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
</span>{
    <span class="hljs-variable">$user</span> = <span class="hljs-title class_">User</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>();

    <span class="hljs-variable">$task</span> = <span class="hljs-title class_">Task</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>([
        <span class="hljs-string">'user_id'</span> =&gt; <span class="hljs-variable">$user</span>-&gt;id,
    ]);

    <span class="hljs-variable">$response</span> = <span class="hljs-variable language_">$this</span>
        -&gt;<span class="hljs-title function_ invoke__">actingAs</span>(<span class="hljs-variable">$user</span>)
        -&gt;<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-title function_ invoke__">route</span>(<span class="hljs-string">'tasks.update'</span>, <span class="hljs-variable">$task</span>-&gt;id), [
            <span class="hljs-string">'name'</span> =&gt; <span class="hljs-literal">null</span>,
            <span class="hljs-string">'priority'</span> =&gt; <span class="hljs-variable">$updatedPriority</span> = <span class="hljs-title class_">Task</span>::<span class="hljs-variable constant_">PRIORITIES</span>[<span class="hljs-title function_ invoke__">array_rand</span>(<span class="hljs-title class_">Task</span>::<span class="hljs-variable constant_">PRIORITIES</span>)],
        ]);

    <span class="hljs-variable">$response</span>
        -&gt;<span class="hljs-title function_ invoke__">assertSessionHasErrors</span>(<span class="hljs-string">'name'</span>);
}

<span class="hljs-comment">/** <span class="hljs-doctag">@test</span> */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">creating_a_task_requires_a_priority</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
</span>{
    <span class="hljs-variable">$user</span> = <span class="hljs-title class_">User</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>();

    <span class="hljs-variable">$task</span> = <span class="hljs-title class_">Task</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">raw</span>([
        <span class="hljs-string">'user_id'</span> =&gt; <span class="hljs-variable">$user</span>-&gt;id,
        <span class="hljs-string">'priority'</span> =&gt; <span class="hljs-literal">null</span>
    ]);

    <span class="hljs-variable">$response</span> = <span class="hljs-variable language_">$this</span>
        -&gt;<span class="hljs-title function_ invoke__">actingAs</span>(<span class="hljs-variable">$user</span>)
        -&gt;<span class="hljs-title function_ invoke__">post</span>(<span class="hljs-title function_ invoke__">route</span>(<span class="hljs-string">'tasks.store'</span>), <span class="hljs-variable">$task</span>);

    <span class="hljs-variable">$response</span>
        -&gt;<span class="hljs-title function_ invoke__">assertSessionHasErrors</span>(<span class="hljs-string">'priority'</span>);

    <span class="hljs-variable language_">$this</span>-&gt;<span class="hljs-title function_ invoke__">assertDatabaseMissing</span>(<span class="hljs-string">'tasks'</span>, <span class="hljs-variable">$task</span>);
}

<span class="hljs-comment">/** <span class="hljs-doctag">@test</span> */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">updating_a_task_requires_a_priority</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
</span>{
    <span class="hljs-variable">$user</span> = <span class="hljs-title class_">User</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>();

    <span class="hljs-variable">$task</span> = <span class="hljs-title class_">Task</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>([
        <span class="hljs-string">'user_id'</span> =&gt; <span class="hljs-variable">$user</span>-&gt;id,
    ]);

    <span class="hljs-variable">$response</span> = <span class="hljs-variable language_">$this</span>
        -&gt;<span class="hljs-title function_ invoke__">actingAs</span>(<span class="hljs-variable">$user</span>)
        -&gt;<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-title function_ invoke__">route</span>(<span class="hljs-string">'tasks.update'</span>, <span class="hljs-variable">$task</span>-&gt;id), [
            <span class="hljs-string">'name'</span> =&gt; <span class="hljs-title function_ invoke__">fake</span>()-&gt;sentence,
            <span class="hljs-string">'priority'</span> =&gt; <span class="hljs-literal">null</span>,
        ]);

    <span class="hljs-variable">$response</span>
        -&gt;<span class="hljs-title function_ invoke__">assertSessionHasErrors</span>(<span class="hljs-string">'priority'</span>);
}

<span class="hljs-comment">/** <span class="hljs-doctag">@test</span> */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">creating_a_task_requires_a_status</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
</span>{
    <span class="hljs-variable">$user</span> = <span class="hljs-title class_">User</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>();

    <span class="hljs-variable">$task</span> = <span class="hljs-title class_">Task</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">raw</span>([
        <span class="hljs-string">'user_id'</span> =&gt; <span class="hljs-variable">$user</span>-&gt;id,
        <span class="hljs-string">'status'</span> =&gt; <span class="hljs-literal">null</span>
    ]);

    <span class="hljs-variable">$response</span> = <span class="hljs-variable language_">$this</span>
        -&gt;<span class="hljs-title function_ invoke__">actingAs</span>(<span class="hljs-variable">$user</span>)
        -&gt;<span class="hljs-title function_ invoke__">post</span>(<span class="hljs-title function_ invoke__">route</span>(<span class="hljs-string">'tasks.store'</span>), <span class="hljs-variable">$task</span>);

    <span class="hljs-variable">$response</span>
        -&gt;<span class="hljs-title function_ invoke__">assertSessionHasErrors</span>(<span class="hljs-string">'status'</span>);

    <span class="hljs-variable language_">$this</span>-&gt;<span class="hljs-title function_ invoke__">assertDatabaseMissing</span>(<span class="hljs-string">'tasks'</span>, <span class="hljs-variable">$task</span>);
}

<span class="hljs-comment">/** <span class="hljs-doctag">@test</span> */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">updating_a_task_requires_a_status</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
</span>{
    <span class="hljs-variable">$user</span> = <span class="hljs-title class_">User</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>();

    <span class="hljs-variable">$task</span> = <span class="hljs-title class_">Task</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>([
        <span class="hljs-string">'user_id'</span> =&gt; <span class="hljs-variable">$user</span>-&gt;id,
    ]);

    <span class="hljs-variable">$response</span> = <span class="hljs-variable language_">$this</span>
        -&gt;<span class="hljs-title function_ invoke__">actingAs</span>(<span class="hljs-variable">$user</span>)
        -&gt;<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-title function_ invoke__">route</span>(<span class="hljs-string">'tasks.update'</span>, <span class="hljs-variable">$task</span>-&gt;id), <span class="hljs-title class_">Task</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">raw</span>([
                <span class="hljs-string">'status'</span> =&gt; <span class="hljs-literal">null</span>
            ]));

    <span class="hljs-variable">$response</span>
        -&gt;<span class="hljs-title function_ invoke__">assertSessionHasErrors</span>(<span class="hljs-string">'status'</span>);
}

<span class="hljs-comment">/** <span class="hljs-doctag">@test</span> */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">a_user_cannot_see_another_users_tasks</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
</span>{
    <span class="hljs-variable">$user1</span> = <span class="hljs-title class_">User</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>();
    <span class="hljs-variable">$user2</span> = <span class="hljs-title class_">User</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>();

    <span class="hljs-variable">$user1Task</span> = <span class="hljs-title class_">Task</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>([
        <span class="hljs-string">'user_id'</span> =&gt; <span class="hljs-variable">$user1</span>-&gt;id,
    ]);

    <span class="hljs-variable">$response</span> = <span class="hljs-variable language_">$this</span>
        -&gt;<span class="hljs-title function_ invoke__">actingAs</span>(<span class="hljs-variable">$user2</span>)
        -&gt;<span class="hljs-title function_ invoke__">get</span>(<span class="hljs-title function_ invoke__">route</span>(<span class="hljs-string">'tasks.index'</span>));

    <span class="hljs-variable">$response</span>
        -&gt;<span class="hljs-title function_ invoke__">assertDontSee</span>(<span class="hljs-variable">$user1Task</span>-&gt;name);
}

<span class="hljs-comment">/** <span class="hljs-doctag">@test</span> */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">a_user_cannot_update_another_users_tasks</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
</span>{
    <span class="hljs-variable">$user1</span> = <span class="hljs-title class_">User</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>();
    <span class="hljs-variable">$user2</span> = <span class="hljs-title class_">User</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>();

    <span class="hljs-variable">$user1Task</span> = <span class="hljs-title class_">Task</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>([
        <span class="hljs-string">'user_id'</span> =&gt; <span class="hljs-variable">$user1</span>-&gt;id,
    ]);

    <span class="hljs-variable">$response</span> = <span class="hljs-variable language_">$this</span>
        -&gt;<span class="hljs-title function_ invoke__">actingAs</span>(<span class="hljs-variable">$user2</span>)
        -&gt;<span class="hljs-title function_ invoke__">put</span>(<span class="hljs-title function_ invoke__">route</span>(<span class="hljs-string">'tasks.update'</span>, <span class="hljs-variable">$user1Task</span>-&gt;id), [
            <span class="hljs-string">'name'</span> =&gt; <span class="hljs-variable">$updatedName</span> = <span class="hljs-title function_ invoke__">fake</span>()-&gt;sentence,
            <span class="hljs-string">'priority'</span> =&gt; <span class="hljs-variable">$updatedPriority</span> = <span class="hljs-title class_">Task</span>::<span class="hljs-variable constant_">PRIORITIES</span>[<span class="hljs-title function_ invoke__">array_rand</span>(<span class="hljs-title class_">Task</span>::<span class="hljs-variable constant_">PRIORITIES</span>)],
        ]);

    <span class="hljs-variable">$response</span>-&gt;<span class="hljs-title function_ invoke__">assertStatus</span>(<span class="hljs-number">403</span>);

    <span class="hljs-variable language_">$this</span>-&gt;<span class="hljs-title function_ invoke__">assertDatabaseMissing</span>(<span class="hljs-string">'tasks'</span>, [
        <span class="hljs-string">'id'</span> =&gt; <span class="hljs-variable">$user1Task</span>-&gt;id,
        <span class="hljs-string">'name'</span> =&gt; <span class="hljs-variable">$updatedName</span>,
        <span class="hljs-string">'priority'</span> =&gt; <span class="hljs-variable">$updatedPriority</span>
    ]);
}

<span class="hljs-comment">/** <span class="hljs-doctag">@test</span> */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">a_user_cannot_delete_another_users_tasks</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
</span>{
    <span class="hljs-variable">$user1</span> = <span class="hljs-title class_">User</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>();
    <span class="hljs-variable">$user2</span> = <span class="hljs-title class_">User</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>();

    <span class="hljs-variable">$task</span> = <span class="hljs-title class_">Task</span>::<span class="hljs-title function_ invoke__">factory</span>()-&gt;<span class="hljs-title function_ invoke__">create</span>([
        <span class="hljs-string">'user_id'</span> =&gt; <span class="hljs-variable">$user1</span>-&gt;id,
    ]);

    <span class="hljs-variable">$response</span> = <span class="hljs-variable language_">$this</span>
        -&gt;<span class="hljs-title function_ invoke__">actingAs</span>(<span class="hljs-variable">$user2</span>)
        -&gt;<span class="hljs-title function_ invoke__">get</span>(<span class="hljs-title function_ invoke__">route</span>(<span class="hljs-string">'tasks.index'</span>));

    <span class="hljs-variable">$response</span>
        -&gt;<span class="hljs-title function_ invoke__">assertDontSee</span>(<span class="hljs-variable">$task</span>-&gt;name);
}

}</pre></div><div id="ace2"><pre><span class="hljs-meta"><?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title class_">Tests</span><span class="hljs-title class_">Unit</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span><span class="hljs-title">Foundation</span><span class="hljs-title">Testing</span><span class="hljs-title">RefreshDatabase</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span><span class="hljs-title">Foundation</span><span class="hljs-title">Testing</span><span class="hljs-title">WithFaker</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span><span class="hljs-title">Support</span><span class="hljs-title">Facades</span><span class="hljs-title">Schema</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">Tests</span><span class="hljs-title">TestCase</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TaskTest</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">TestCase</span> </span>{ <span class="hljs-keyword">use</span> <span class="hljs-title">RefreshDatabase</span>; <span class="hljs-keyword">use</span> <span class="hljs-title">WithFaker</span>;

<span class="hljs-comment">/** <span class="hljs-doctag">@test</span>  */</span>
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">it_has_expected_columns</span>(<span class="hljs-params"></span>): <span class="hljs-title">void</span>
</span>{
    <span class="hljs-variable language_">$this</span>-&gt;<span class="hljs-title function_ invoke__">assertTrue</span>(
        <span class="hljs-title class_">Schema</span>::<span class="hljs-title function_ invoke__">hasColumns</span>(<span class="hljs-string">'tasks'</span>, [
            <span class="hljs-string">'name'</span>,
            <span class="hljs-string">'priority'</span>,
            <span class="hljs-string">'user_id'</span>
        ])
    );
}

}</pre></div><p id="9d11">Run the tests and you should see the following:</p><figure id="a5b4"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*5ZFEbjT9qTg8-RNIwGlXMw.png"><figcaption>Test results</figcaption></figure><h1 id="8fd5">Conclusion</h1><p id="3272">Creating a SPA CRUD application using Laravel, Vue, and Breeze is a straightforward process that combines the power of Laravel’s backend capabilities with the reactivity of Vue.js. Breeze accelerates this by providing a simple authentication scaffold. By following this guide, you’ll have a solid foundation to expand your SPA further, tailoring it to your specific needs.</p><h1 id="30e1">Resources</h1><p id="9a06">Github: <a href="https://github.com/shaunthornburgh/spa-crud-todo-app">https://github.com/shaunthornburgh/spa-crud-todo-app</a></p></article></body>

Creating a SPA CRUD Application using Laravel, Vue, and Breeze

Single Page Applications (SPAs) have revolutionised the way users experience the web. By making interactions smoother and faster, SPAs enhance user engagement and satisfaction. If you’re looking to develop a SPA CRUD (Create, Read, Update, Delete) application, the combination of Laravel, Vue.js, and Breeze offers a streamlined way to do it. To demonstrate we can create a simple todo app.

The Tech Stack

Laravel — Laravel is a free, open-source PHP web framework created by Taylor Otwell and was initially released in June 2011. It was designed to develop web applications following the model–view–controller (MVC) architectural pattern. Laravel has become one of the most popular and widely used PHP frameworks because of its elegant syntax, a wide array of features, and active community support.

Breeze — Laravel Breeze, introduced in late 2020, is a minimal and simple starting point for building a Laravel application with authentication. Designed by the Laravel team, Breeze provides a basic implementation of all of Laravel’s authentication features, including login, registration, password reset, email verification, and password confirmation.

Inertia.js — Inertia.js is a framework used to create single-page apps (SPAs) without requiring the complexity of a full front-end framework like Vue, React, or Svelte. The primary goal of Inertia.js is to allow developers to build server-driven SPAs. This means that the bulk of the application logic, routing, and data management is handled on the server side, while the client side (browser) primarily deals with rendering views and handling user interactions. Inertia blends the benefits of traditional server-side rendering (like you’d find in Laravel or Django apps) with the benefits of SPAs, giving you faster page loads and a more dynamic user experience.

Vue.js — Vue.js is a progressive JavaScript framework for building user interfaces. Unlike other monolithic frameworks, Vue is designed from the ground up to be incrementally adoptable. This means you can use as much or as little of Vue as you want, depending on the needs of your project.

Let’s get started…

Step 1: Install Laravel

If you’re on a mac you can get up and running very quickly with the following command which will also create the necessary Docker containers for you:

curl -s "https://laravel.build/spa-crud-todo-app" | bash

Once finished run the following to start the containers:

cd spa-crud-todo-app && ./vendor/bin/sail up

Once the application’s Docker containers have been started, you can access the application in your web browser at: http://localhost.

New Laravel installation

Step 2: Install Breeze

Breeze offers a minimal and simple starting point for building a Laravel application with authentication. To install Breeze:

sail composer require laravel/breeze --dev

Run the following artisan command to configure the Breeze installation:

sail artisan breeze:install

These are options I selected, you can choose your if you wish:

Next we need to compile front end assets and run migrations:

sail artisan migrate
sail npm install
sail npm run dev

We now have auth routes and you can navigate to /login or /register URLs in your web browser.

Step 3. Creating CRUD Functionality

Let’s start work on the backend for our todo app. First of all, create a model to store tasks:

sail artisan make:model Task -m

This will create a model and a migration. Modify the generated migration to have the columns you need (e.g., ‘name’, ‘notes’ and ‘priority’).

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->enum('priority', [
                'High',
                'Medium',
                'Low'
            ]);
            $table->enum('status', [
                'Not started',
                'In progress',
                'Completed'
            ]);
            $table->date('due_by');
            $table->foreignId('user_id')->constrained();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('tasks');
    }
};

Run migrations:

sail artisan migrate

Next we can create a controller to manage this model.

sail artisan make:controller TaskController --resource

This command will generate a controller at app/Http/Controllers/TaskController.php. You can delete the show,create and edit functions as we won’t need these. The controller will contain a method for each of the available resource operations. Next, register a resource route that points to the controller, adding theauth middleware.

Route::resource('tasks', TaskController::class)
        ->only(['index', 'store', 'update', 'destroy'])
        ->middleware(['auth', 'verified']);

Let’s create some constants in our Task model as we will need these in our app, we will also define the fields that are available for mass assignment.

class Task extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'priority',
        'status',
        'user_id',
        'due_by'
    ];

    public const PRIORITY_HIGH = 'high';
    public const PRIORITY_MEDIUM = 'medium';
    public const PRIORITY_LOW = 'low';

    public const PRIORITIES = [
        self::PRIORITY_HIGH,
        self::PRIORITY_MEDIUM,
        self::PRIORITY_LOW,
    ];

    public const STATUS_NOT_STARTED = 'Not started';
    public const STATUS_IN_PROGRESS = 'In progress';
    public const STATUS_COMPLETED = 'Completed';

    public const STATUSES = [
        self::STATUS_NOT_STARTED,
        self::STATUS_IN_PROGRESS,
        self::STATUS_COMPLETED,
    ];
}

Next create Form Request for validation. Laravel’s Form Request is a custom request class that encapsulates validation logic. Instead of defining validation rules and messages directly within a controller method, you can move this logic to a Form Request to simplify and organize your code, especially for larger applications with many validation rules. To create a form request, run:

sail artisan make:request StoreTaskRequest

Add the following rules:

public function rules(): array
{
    return [
        'name' => 'required|string|min:3|max:255',
        'priority' => 'required|in:' . implode(',',Task::PRIORITIES),
        'status' => 'required|in:' . implode(',',Task::STATUSES),
        'due_by' => 'required|after:' . date('Y-m-d')
    ];
}

We also need to define the relationship between User and Task. Add the following method to the User class.

public function tasks(): HasMany
{
    return $this->hasMany(Task::class);
}

This tells Laravel that in our application, each User can have many Tasks, but a task will only belong to one User.

We also need to lock add some logic to ensure only authorised users can perform operations on their own Tasks. This is a great use case for Laravel Policies. Policies are classes that organise authorisation logic around a particular model or resource. For instance, if you have a Post model in a blogging platform, you might have a corresponding PostPolicy to determine what actions a user can perform on a post (e.g., viewing, updating, or deleting). To create a Policy run the following:

sail artisan make:policy TaskPolicy

Register the policy in App\Providers\AuthServiceProvider:

protected $policies = [
    Task::class => TaskPolicy::class,
];

We can now create our update TaskController:

class TaskController extends Controller
{
    public function __construct() {
        $this->authorizeResource(Task::class, 'task');
    }

    public function index(): Response
    {
        return inertia(
            'Task/Index',
            [
                'tasks' => Auth::user()->tasks
            ]
        );
    }

    public function store(StoreTaskRequest $request)
    {
        Auth::user()->tasks()->create($request->validated());

        return Redirect::route('tasks.index')->with('success', 'Task created.');
    }

    public function update(StoreTaskRequest $request, Task $task)
    {
        $task->update($request->validated());

        return Redirect::route('tasks.index')->with('success', 'Task updated.');
    }

    public function destroy(Task $task)
    {
        $task->delete();

        return Redirect::route('tasks.index')->with('success', 'Task deleted.');
    }
}

The Task/Index component hasn’t been created yet, so let’s move on to the front end and do that in the next section. Before we do that let’s just tidy up ur routes. We can update our app so that users are directed to the tasks page when they login.

Delete dashboard route, and update HOME in App\Providers\RouteServiceProvider:

public const HOME = '/tasks';

We can also remove the dashboard and welcome routes in routes/web.php. Your file should now look like this:

Route::resource('tasks', TaskController::class)
    ->only(['index', 'store', 'update', 'destroy'])
    ->middleware(['auth', 'verified']);

Route::middleware('auth')->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});

Finally, we can delete the welcome page, and direct users to the login page, add the following line to auth.php:

Route::get('/', [AuthenticatedSessionController::class, 'create'])
    ->name('login');

Step 4: Building The Front End

I would like this page to be as simple as possible, we will just have a list of tasks, with a modal for adding and editing tasks. But before we start, let’s delete Dashboard and Welcome components. We can also remove welcome.blade.php.

Create Pages/Task/Index.vue with the following:

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { Head } from '@inertiajs/vue3';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import { useForm } from '@inertiajs/vue3';
import TableRow from "@/Pages/Task/Components/TableRow.vue";
import TaskForm from "@/Pages/Task/Components/TaskForm.vue";
import {ref} from "vue";
import Modal from '@/Components/Modal.vue';
import DangerButton from "@/Components/DangerButton.vue";
import SecondaryButton from "@/Components/SecondaryButton.vue";

const form = useForm({});
const showFormModal = ref(false);
const showDeleteConfirmationModal = ref(false);
const selectedTask = ref(null);

defineProps({
    tasks: Object
})

const openFormModal = (task) => {
    selectedTask.value = task;
    showFormModal.value = true;
}

const closeFormModal = () => {
    showFormModal.value = false;
    selectedTask.value = null;
    form.reset();
};

const openDeleteConfirmationModal = (task) => {
    selectedTask.value = task;
    showDeleteConfirmationModal.value = true;
}

const closeDeleteConfirmationModal = () => {
    showDeleteConfirmationModal.value = false;
    selectedTask.value = null;
};

const deleteTask = () => {
    form.delete(route('tasks.destroy', {'task': selectedTask.value.id}))
    closeDeleteConfirmationModal()
}

</script>

<template>
    <Head title="Tasks" />

    <AuthenticatedLayout>
        <template #header>
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">Tasks</h2>
        </template>
        <div class="py-12">
            <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
                <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg pt-8">
                    <div class="px-4 sm:px-6 lg:px-8">
                        <div class="sm:flex sm:items-center">
                            <div class="sm:flex-auto">
                                <h1 class="text-lg font-medium text-gray-900">Tasks</h1>
                                <p class="mt-1 text-sm text-gray-600">A list of all your tasks with the name, status, priority and due by date.</p>
                            </div>
                            <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
                                <PrimaryButton @click="openFormModal({})">Add Task</PrimaryButton>
                            </div>
                        </div>
                        <div class="mt-8 flow-root">
                            <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
                                <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
                                    <table class="min-w-full divide-y divide-gray-300">
                                        <thead>
                                            <tr>
                                                <th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0 w-1/2">Name</th>
                                                <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Priority</th>
                                                <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Status</th>
                                                <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Due By</th>
                                                <th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0">
                                                    <span class="sr-only">Edit</span>
                                                </th>
                                                <th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0">
                                                    <span class="sr-only">Delete</span>
                                                </th>
                                            </tr>
                                        </thead>
                                        <tbody class="divide-y divide-gray-200">
                                            <table-row
                                                v-for="task in tasks"
                                                :key="task.id"
                                                :task="task"
                                                @editClicked="openFormModal(task)"
                                                @deleteClicked="openDeleteConfirmationModal(task)"
                                            />
                                        </tbody>
                                    </table>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <Modal :show="showFormModal">
            <TaskForm :task="selectedTask" @close="closeFormModal"/>
        </Modal>
        <Modal :show="showDeleteConfirmationModal" @close="closeDeleteConfirmationModal">
            <div class="p-6">
                <h2 class="text-lg font-medium text-gray-900">
                    Are you sure you want to delete this task?
                </h2>

                <p class="mt-1 text-sm text-gray-600">
                    Once your task is deleted, all of it's data will be permanently deleted. Please
                    click the delete button to confirm.
                </p>

                <div class="mt-6 flex justify-end">
                    <SecondaryButton @click="closeDeleteConfirmationModal"> Cancel </SecondaryButton>

                    <DangerButton
                        class="ml-3"
                        :class="{ 'opacity-25': form.processing }"
                        :disabled="form.processing"
                        @click="deleteTask"
                    >
                        Delete Task
                    </DangerButton>
                </div>
            </div>
        </Modal>
    </AuthenticatedLayout>
</template>

We are using a number of components that are included with the Laravel Breeze starter kit. However I have created a component for creating and editing a task called TaskForm.vue:

<script setup>
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import {useForm} from "@inertiajs/vue3";
import PrimaryButton from '@/Components/PrimaryButton.vue';
import VueTailwindDatepicker from 'vue-tailwind-datepicker';

const props = defineProps({
    task: Object
})

const form = useForm({
    id: props.task.id,
    name: props.task.name || '',
    priority: props.task.priority || '',
    status: props.task.status || '',
    due_by: props.task.due_by || ''
})

const emit = defineEmits(['close']);

const closeModal = () => {
    form.reset();
    emit('close');
}

const store = () => {
    form.post(route('tasks.store'), {
        onSuccess: () => closeModal(),
    });
}

const update = () => {
    form.put(route('tasks.update', {'task': form.id}), {
        onSuccess: () => closeModal(),
    });
}

const editing = () => {
    return Object.keys(props.task).length
}

</script>

<template>
    <div class="p-6">
        <h2 class="text-lg font-medium text-gray-900">
            {{ editing() ? 'Edit' : 'Create '}} a task.
        </h2>

        <p class="mt-1 text-sm text-gray-600">
            Enter the details below for your task and click enter to create.
        </p>

        <div class="mt-6">
            <InputLabel for="name" value="Name"  />

            <TextInput
                id="name"
                ref="nameInput"
                v-model="form.name"
                type="text"
                class="mt-1 block w-3/4"
                placeholder="Name"
            />

            <InputError :message="form.errors.name" class="mt-2" />
        </div>

        <div class="mt-6">
            <InputLabel for="status" value="Status"  />

            <select
                class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
                id="status"
                name="status"
                v-model="form.status"
            >
                <option>Not started</option>
                <option>In progress</option>
                <option>Completed</option>
            </select>

            <InputError :message="form.errors.status" class="mt-2" />
        </div>

        <div class="mt-6">
            <InputLabel for="priority" value="Priority"  />

            <select
                class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
                name="status"
                id="status"
                v-model="form.priority"
            >
                <option>Low</option>
                <option>Medium</option>
                <option>High</option>
            </select>

            <InputError :message="form.errors.priority" class="mt-2" />
        </div>

        <div class="mt-6">
            <InputLabel for="due_by" value="Due By" />

            <vue-tailwind-datepicker
                as-single
                v-model="form.due_by"
                placeholder="Due by"
                no-input
                name="due_by"
            />

            <InputError :message="form.errors.due_by" class="mt-2" />
        </div>

        <div class="mt-6 flex justify-end">
            <SecondaryButton @click="closeModal"> Cancel </SecondaryButton>

            <PrimaryButton
                v-if="editing()"
                class="ml-3"
                :class="{ 'opacity-25': form.processing }"
                :disabled="form.processing"
                @click="update"
            >
                Save Changes
            </PrimaryButton>

            <PrimaryButton
                v-else
                class="ml-3"
                :class="{ 'opacity-25': form.processing }"
                :disabled="form.processing"
                @click="store"
            >
                Create Task
            </PrimaryButton>
        </div>
    </div>
</template>

I have also created a compenent for each row in the table of tasks called TaskRow.vue.

<script setup>
import SecondaryButton from "@/Components/SecondaryButton.vue";
import Red from "@/Components/Badges/Red.vue";
import Yellow from "@/Components/Badges/Yellow.vue";
import Green from "@/Components/Badges/Green.vue";
import DangerButton from "@/Components/DangerButton.vue";

const props = defineProps({
    task: Object
})

const emit = defineEmits(['editClicked', 'deleteClicked'])

const handleEditClick = () => {
    emit('editClicked', props.task);
}

const handleDeleteClick = () => {
    emit('deleteClicked', props.task);
}
</script>

<template>
    <tr>
        <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">{{ props.task.name }}</td>
        <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
            <red v-if="props.task.priority === 'High'">{{ props.task.priority }}</red>
            <yellow v-if="props.task.priority === 'Medium'">{{ props.task.priority }}</yellow>
            <green v-if="props.task.priority === 'Low'">{{ props.task.priority }}</green>
        </td>
        <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
            <red v-if="props.task.status === 'Not started'">{{ props.task.status }}</red>
            <yellow v-if="props.task.status === 'In progress'">{{ props.task.status }}</yellow>
            <green v-if="props.task.status === 'Completed'">{{ props.task.status }}</green>
        </td>
        <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{{ props.task.due_by }}</td>
        <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
            <SecondaryButton @click="handleEditClick">Edit</SecondaryButton>
        </td>
        <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
            <DangerButton @click="handleDeleteClick">Delete</DangerButton>
        </td>
    </tr>
</template>

Let’s create some test data. Create a factory class so we can populdate the tasks table:

sail artisan make:factory TaskFactory
<?php

namespace Database\Factories;

use App\Models\Task;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends Factory<Task>
 */
class TaskFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'name' => fake()->sentence,
            'priority' => Task::PRIORITIES[array_rand(Task::PRIORITIES)],
            'status' => Task::STATUSES[array_rand(Task::STATUSES)],
            'user_id' => User::factory(),
            'due_by' => fake()->dateTimeBetween('+0 days', '+2 weeks')->format('Y-m-d')
        ];
    }
}

Head over to Tinker and run the following:

This will create a user with 20 tasks assigned to them. Log in with this user and this is what we see:

Task list

And this is the Task form:

You will notice I have used a date picker for the due_by field, full details for this package can be found here https://vue-tailwind-datepicker.com/

Step 5: Write some Tests

Let’s write some tests to ensure our code is functioning as expetcted. Test-Driven Development (TDD) is a software development practice that emphasises writing tests before writing the actual code for a software component or feature. The core idea is to shift the focus from just writing code to writing testable, maintainable, and clean code.

<?php

namespace Tests\Feature;

use App\Models\Task;
use App\Models\User;
use Database\Factories\TaskFactory;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use JsonException;
use Tests\TestCase;

class TaskTest extends TestCase
{
    use RefreshDatabase;
    use WithFaker;

    /** @test */
    public function a_user_can_view_their_tasks(): void
    {
        $user = User::factory()->create();

        $task = Task::factory()->create(['user_id' => $user->id]);

        $this
            ->actingAs($user)
            ->get(route('tasks.index'))
            ->assertSee($task->name);
    }

    /** @test
     * @throws JsonException
     */
    public function a_user_can_create_a_task(): void
    {
        $user = User::factory()->create();

        $task = Task::factory()->raw(['user_id' => $user->id]);

        $response = $this
            ->actingAs($user)
            ->post(route('tasks.store'), $task);

        $response
            ->assertSessionHasNoErrors()
            ->assertRedirect(route('tasks.index'));

        $this->assertDatabaseHas('tasks', $task);
    }

    /** @test */
    public function a_user_can_update_a_task(): void
    {
        $user = User::factory()->create();

        $task = Task::factory()->create(['user_id' => $user->id]);

        $response = $this
            ->actingAs($user)
            ->put(route('tasks.update', $task->id), [
                'name' => $updatedName = fake()->sentence,
                'priority' => $updatedPriority = Task::PRIORITIES[array_rand(Task::PRIORITIES)],
                'status' => $updatedStatus = Task::STATUSES[array_rand(Task::STATUSES)],
            ]);

        $response
            ->assertSessionHasNoErrors()
            ->assertRedirect(route('tasks.index'));

        $this->assertDatabaseHas('tasks', [
            'id' => $task->id,
            'name' => $updatedName,
            'priority' => $updatedPriority,
            'status' => $updatedStatus,
        ]);
    }

    /** @test */
    public function a_user_can_delete_a_task(): void
    {
        $user = User::factory()->create();

        $task = Task::factory()->create(['user_id' => $user->id]);

        $response = $this
            ->actingAs($user)
            ->delete(route('tasks.destroy', $task->id));

        $response
            ->assertSessionHasNoErrors()
            ->assertRedirect(route('tasks.index'));

        $this->assertDatabaseMissing('tasks', $task->toArray());
    }

    /** @test */
    public function creating_a_task_requires_a_name(): void
    {
        $user = User::factory()->create();

        $task = Task::factory()->raw([
            'user_id' => $user->id,
            'name' => null
        ]);

        $response = $this
            ->actingAs($user)
            ->post(route('tasks.store'), $task);

        $response
            ->assertSessionHasErrors('name');

        $this->assertDatabaseMissing('tasks', $task);
    }

    /** @test */
    public function updating_a_task_requires_a_name(): void
    {
        $user = User::factory()->create();

        $task = Task::factory()->create([
            'user_id' => $user->id,
        ]);

        $response = $this
            ->actingAs($user)
            ->put(route('tasks.update', $task->id), [
                'name' => null,
                'priority' => $updatedPriority = Task::PRIORITIES[array_rand(Task::PRIORITIES)],
            ]);

        $response
            ->assertSessionHasErrors('name');
    }

    /** @test */
    public function creating_a_task_requires_a_priority(): void
    {
        $user = User::factory()->create();

        $task = Task::factory()->raw([
            'user_id' => $user->id,
            'priority' => null
        ]);

        $response = $this
            ->actingAs($user)
            ->post(route('tasks.store'), $task);

        $response
            ->assertSessionHasErrors('priority');

        $this->assertDatabaseMissing('tasks', $task);
    }

    /** @test */
    public function updating_a_task_requires_a_priority(): void
    {
        $user = User::factory()->create();

        $task = Task::factory()->create([
            'user_id' => $user->id,
        ]);

        $response = $this
            ->actingAs($user)
            ->put(route('tasks.update', $task->id), [
                'name' => fake()->sentence,
                'priority' => null,
            ]);

        $response
            ->assertSessionHasErrors('priority');
    }

    /** @test */
    public function creating_a_task_requires_a_status(): void
    {
        $user = User::factory()->create();

        $task = Task::factory()->raw([
            'user_id' => $user->id,
            'status' => null
        ]);

        $response = $this
            ->actingAs($user)
            ->post(route('tasks.store'), $task);

        $response
            ->assertSessionHasErrors('status');

        $this->assertDatabaseMissing('tasks', $task);
    }

    /** @test */
    public function updating_a_task_requires_a_status(): void
    {
        $user = User::factory()->create();

        $task = Task::factory()->create([
            'user_id' => $user->id,
        ]);

        $response = $this
            ->actingAs($user)
            ->put(route('tasks.update', $task->id), Task::factory()->raw([
                    'status' => null
                ]));

        $response
            ->assertSessionHasErrors('status');
    }

    /** @test */
    public function a_user_cannot_see_another_users_tasks(): void
    {
        $user1 = User::factory()->create();
        $user2 = User::factory()->create();

        $user1Task = Task::factory()->create([
            'user_id' => $user1->id,
        ]);

        $response = $this
            ->actingAs($user2)
            ->get(route('tasks.index'));

        $response
            ->assertDontSee($user1Task->name);
    }

    /** @test */
    public function a_user_cannot_update_another_users_tasks(): void
    {
        $user1 = User::factory()->create();
        $user2 = User::factory()->create();

        $user1Task = Task::factory()->create([
            'user_id' => $user1->id,
        ]);

        $response = $this
            ->actingAs($user2)
            ->put(route('tasks.update', $user1Task->id), [
                'name' => $updatedName = fake()->sentence,
                'priority' => $updatedPriority = Task::PRIORITIES[array_rand(Task::PRIORITIES)],
            ]);

        $response->assertStatus(403);

        $this->assertDatabaseMissing('tasks', [
            'id' => $user1Task->id,
            'name' => $updatedName,
            'priority' => $updatedPriority
        ]);
    }

    /** @test */
    public function a_user_cannot_delete_another_users_tasks(): void
    {
        $user1 = User::factory()->create();
        $user2 = User::factory()->create();

        $task = Task::factory()->create([
            'user_id' => $user1->id,
        ]);

        $response = $this
            ->actingAs($user2)
            ->get(route('tasks.index'));

        $response
            ->assertDontSee($task->name);
    }
}
<?php

namespace Tests\Unit;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;

class TaskTest extends TestCase
{
    use RefreshDatabase;
    use WithFaker;

    /** @test  */
    public function it_has_expected_columns(): void
    {
        $this->assertTrue(
            Schema::hasColumns('tasks', [
                'name',
                'priority',
                'user_id'
            ])
        );
    }
}

Run the tests and you should see the following:

Test results

Conclusion

Creating a SPA CRUD application using Laravel, Vue, and Breeze is a straightforward process that combines the power of Laravel’s backend capabilities with the reactivity of Vue.js. Breeze accelerates this by providing a simple authentication scaffold. By following this guide, you’ll have a solid foundation to expand your SPA further, tailoring it to your specific needs.

Resources

Github: https://github.com/shaunthornburgh/spa-crud-todo-app

Laravel
Vuejs
Recommended from ReadMedium