alanmcknee
alanmcknee

Reputation: 199

Laravel + Vue, best practices

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.

Upvotes: 1

Views: 311

Answers (1)

SophiaKCruz
SophiaKCruz

Reputation: 26

  1. Sending data as a prop using JSON is fine to initialize the component, just make sure there's proper encoding and decoding where it's needed.
  2. You can't modify a prop directly, but once you've assigned the value from the prop to the the local data, you can pretty much do what you want with that local data. After its been mounted you can actually assign the values of your JSON data to individual components of the same type. Within these components, you can actually emit the data back to the parent if needed. For example if a you modify a value in the child component, you can emit it back to the parent so that the parent will always have an updated value in its local data, and since vue is reactive it'll also assign that value back as a prop to the child. So indirectly you would be modifying the prop. I would suggest creating a component for it because it'll make it much easier for you to create and delete instances of it and minimize the amount of work you need to do on the parent level.
  3. For the calculations, vue has a calculated property that you can use for calculating a value and assigning it to data that can be display in your component. Be careful with this because if your calculation is to big it can really slow things down. There's also watchers you can try to play around with to solve this problem, but I think calculations would be easiest for total price. Computed Vs Watched Documentation
  1. It's really nice that you're trying to learn best practices. I am also still learning. The only thing I would suggest so far is to create a new component type and initialize them as you create and add new ones to your list. It'll be easier for manipulation and deletion as well.

Upvotes: 1

Related Questions