Laravel + Vue, best practices

  javascript, laravel, php, vue.js

I am building a simple invoice system in Laravel. So obviously I need to build a view, when I will be able to edit it. It is a table, where I can dynamically add and remove rows.

So my thinking was: ok, I can do that quite easily with jQuery, but adding a row with multiple inputs, especially when I’m using tailwind, meaning having a lot of weird classes, will be messy, so I’ll try with Vue. I have no experience with it, but in general it looks easy.

I made a Vue component then, which contains <table>, and <tr>s with inputs inside:

<document-items-table :items='@json($document->items)' />

It’s not SPA, so I didn’t want to make AJAX call inside, I have my document already loaded so I passed document items through a vue prop as Json. And it works fine.

Next thing is, to every document line I’ve added delete button which deletes a line. I’ve got also a button which adds an empty line.

My component looks like this:

<template>
    <div class="w-full">
        <div class="relative flex flex-col min-w-0 break-words w-full rounded bg-white">
            <div class="block w-full overflow-x-auto">
                <table class="items-center w-full bg-transparent border-collapse pb-4">
                    <thead>
                    <tr>
                        <th style="width: 30px" class="pl-6 pr-2 align-middle border-b border-solid py-3 text-xs uppercase border-l-0 border-r-0 whitespace-nowrap font-semibold text-left bg-blueGray-50 text-blueGray-500 border-blueGray-100">
                            No.
                        </th>
                        <th style="min-width: 600px" class=" align-middle border-b border-solid py-3 text-xs uppercase border-l-0 border-r-0 whitespace-nowrap font-semibold text-left bg-blueGray-50 text-blueGray-500 border-blueGray-100">
                            Name
                        </th>
                        <th style="width: 60px" class="align-middle border-b border-solid py-3 text-xs uppercase border-l-0 border-r-0 whitespace-nowrap font-semibold text-left bg-blueGray-50 text-blueGray-500 border-blueGray-100">
                            Quantity
                        </th>
                        <th style="width: 60px"  class="align-middle border-b border-solid py-3 text-xs uppercase border-l-0 border-r-0 whitespace-nowrap font-semibold text-left bg-blueGray-50 text-blueGray-500 border-blueGray-100">
                            Unit
                        </th>
                        <th style="width: 120px"  class="align-middle border-b border-solid py-3 text-xs uppercase border-l-0 border-r-0 whitespace-nowrap font-semibold text-left bg-blueGray-50 text-blueGray-500 border-blueGray-100">
                            Price
                        </th>
                        <th style="width: 60px"  class="align-middle border-b border-solid py-3 text-xs uppercase border-l-0 border-r-0 whitespace-nowrap font-semibold text-left bg-blueGray-50 text-blueGray-500 border-blueGray-100">
                            Tax rate
                        </th>
                        <th style="width: 60px"  class="align-middle border-b border-solid py-3 text-xs uppercase border-l-0 border-r-0 whitespace-nowrap font-semibold text-left bg-blueGray-50 text-blueGray-500 border-blueGray-100">

                        </th>
                    </tr>
                    </thead>
                    <tbody class="border-b-4 border-white">
                    <tr v-for="(item, i) in this.itemsLocal" :key="item.id">
                        <td class="border-t-0 pl-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap py-1">
                            {{ i + 1 }}
                        </td>
                        <td class="border-t-0 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap py-1">
                            <item-input name="title" id="title" :value="item.title" />
                        </td>
                        <td class="border-t-0 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap py-1">
                            <item-input class="w-full" name="quantity" id="quantity" :value="item.quantity" />
                        </td>
                        <td class="border-t-0 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap py-1">
                            <item-input class="w-full" name="unit" id="unit" :value="item.unit" />
                        </td>
                        <td class="border-t-0 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap py-1">
                            <item-input class="w-full" name="price" id="price" :value="item.price / 100" />
                        </td>
                        <td class="border-t-0 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap py-1">
                            <item-input class="w-full" name="tax" id="tax" :value="item.tax_rate" />
                        </td>
                        <td class="border-t-0 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap py-1">
                            <button @click="() => deleteRow(item.id)" class="p-2 px-4 bg-rose-500 text-white rounded"><i class="fas fa-times fa-sm"></i> </button>
                        </td>
                    </tr>
                    </tbody>
                </table>
            </div>
        </div>
        <div class="w-full text-center">
            <button @click="addRow" class="bg-lightBlue-600 px-4 py-2 text-white rounded text-sm font-bold">Add row</button>
        </div>
    </div>
</template>

<script>
export default {
    props: {
        items: Array,
    },
    mounted() {
    },

    data () {
        return {
            itemsLocal: [...this.items],
            newRowCount: 1
        }
    },
    methods: {
        deleteRow(itemID) {
            this.itemsLocal = _.reject(this.itemsLocal, ['id', itemID]);
        },
        addRow() {
            this.itemsLocal.push({
                id: -this.newRowCount
            })
            this.newRowCount++;
        }
    }
}
</script>

Now I have some questions to the people more experienced with Vue

  1. Is passing an array of PHP objects to Vue component using json a clean solution?
  2. Since modifying props isn’t allowed, and I need to add and remove rows, I’m cloning my items from props to data and then I’m adding and deleting them. Is there a better solution for that?
  3. Next to my table, I’ll have some summary box with some "Total price" of all elements. I want to update this value dynamically basing on values I put into those inputs. I could make it a separate component, but I know there’s no a good way to pass values between two components, so how should I solve it? Should I make one more parent-wrapper component that contains both my table and summary box, emit data up to that wrapper and then down to summary component? Or just use some jQuery and don’t bother?
  4. Do you see any other wrong practices I used here? (I am aware of html ids and names duplicated – I will handle it)

I try to make it as clean as possible. It’s not a work project, it’s rather a thing to improve my skills.

Source: Laravel

Leave a Reply