When I get all the records it works:
...
$items = Item::all();
return Inertia::render('Rentals/Items', ['items' => $items]
);
But when I try to paginate, it breaks down:
...
$items = Item::paginate(15);
return Inertia::render('Rentals/Items', ['items' => $items]
);
I get this error:
Uncaught (in promise) TypeError: Cannot read property 'id' of null
Help please. What am I doing wrong? I've been stuck for days.
Creator of Inertia.js here. 👋
So, you can totally use the Laravel paginator with Inertia, you just need to setup your page components to be compatible.
First, make sure you're only returning the items data to the client that you actually need. You can use the new pagination through method for this.
$items = Item::paginate(15)->through(function ($item) {
return [
'id' => $item->id,
'name' => $item->name,
// etc
];
});
return Inertia::render('Rentals/Items', ['items' => $items]);
Next, client side, you'll need a pagination component to actually display the pagination links. Here is an example component from the Ping CRM demo app, built using Tailwind CSS.
<template>
<div v-if="links.length > 3">
<div class="flex flex-wrap -mb-1">
<template v-for="(link, key) in links">
<div v-if="link.url === null" :key="key" class="mr-1 mb-1 px-4 py-3 text-sm leading-4 text-gray-400 border rounded" v-html="link.label" />
<inertia-link v-else :key="key" class="mr-1 mb-1 px-4 py-3 text-sm leading-4 border rounded hover:bg-white focus:border-indigo-500 focus:text-indigo-500" :class="{ 'bg-white': link.active }" :href="link.url" v-html="link.label" />
</template>
</div>
</div>
</template>
<script>
import {InertiaLink} from "#inertiajs/inertia-vue3";
export default {
props: {
links: Array,
},
components : {
InertiaLink
}
}
</script>
Finally, to display the items and the pagination links in your page component, use the items.data and items.links props. Something like this:
<template>
<div>
<div v-for="item in items.data" :key="item.id">
{{ item.name }}
</div>
<pagination :links="items.links" />
</div>
</template>
<script>
import Pagination from '#/Shared/Pagination'
export default {
components: {
Pagination,
},
props: {
items: Object,
},
}
</script>
You can find a full working example of this in the Ping CRM demo app. 👍
Just to keep this post updated I'm adding what works for me using vue 3, laravel 8 and inertia 0.10:
In Pagination component I must change the :key attribute just as #AdiMadhava said before, in addition I must use Link component in order to make the page links usables, so my entire #/Shared/Pagination.vue looks like:
<template>
<div v-if="links.length > 3">
<div class="flex flex-wrap mt-8">
<template v-for="(link, key) in links" :key="key">
<div
v-if="link.url === null"
class="mr-1 mb-1 px-4 py-3 text-sm leading-4 text-gray-400 border rounded"
v-html="link.label"
/>
<Link
v-else
class="mr-1 mb-1 px-4 py-3 text-sm leading-4 border rounded hover:bg-white focus:border-primary focus:text-primary"
:class="{ 'bg-white': link.active }"
:href="link.url"
v-html="link.label"
/>
</template>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import { Link } from "#inertiajs/inertia-vue3";
export default defineComponent({
components: {
Link,
},
props: {
links: Array,
},
});
</script>
And it works accurately.
Related
I managed to make a new project with inertiajs new version and Laravel 8 and vue 3. I want to make a toast component which will show when flash message has value in the handleinertia middleware.
My only problem is I can't seem to get the session value it is null all the time in usePage();. BUT the
{{ $page.props.flash.message }}
in Notification.vue displays it.
My question is, how can I get the session over router.on("finish") so that I can only show it when there is session.
I left some comments in the code, I can't explain well in English :-C
Controller:
return redirect()->route('users')->with('message', 'User created');
HandleInertiaRequests.php
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'flash' => [
'message' => fn () => $request->session()->get('message'),
'alert_message' => fn () => $request->session()->get('alert_message'),
'error' => "check",
],
]);
}
Notification .vue
<script setup>
import { ref, watch, onUnmounted, onMounted } from 'vue'
import { CheckCircleIcon } from '#heroicons/vue/24/outline'
import { XMarkIcon } from '#heroicons/vue/20/solid'
import { usePage, router } from '#inertiajs/vue3'
const page = usePage();
const show = ref(false)
let removeFinshEventListener = router.on("finish", () => {
// I ALWAYS GET NULL VALUES HERE BUT IN THE HTML BELOW I GET THE SESSION
console.log(page.props.flash, page.props.flash.message)
show.value = true
if (page.props.flash.message) {
}
});
onUnmounted(() => removeFinshEventListener());
function remove(index) {
toast.remove(index);
}
</script>
<template>
<!-- Global notification live region, render this permanently at the end of the document -->
<div aria-live="assertive" class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6">
<div class="flex w-full flex-col items-center space-y-4 sm:items-end">
<!-- Notification panel, dynamically insert this into the live region when it needs to be displayed -->
<transition
enter-active-class="transform ease-out duration-300 transition"
enter-from-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2" enter-to-class="translate-y-0 opacity-100 sm:translate-x-0"
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div v-if="show" class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5">
<div class="p-4">
<div class="flex items-start">
<div class="flex-shrink-0">
<CheckCircleIcon class="h-6 w-6 text-green-400" aria-hidden="true" />
</div>
<div class="ml-3 w-0 flex-1 pt-0.5">
<p class="text-sm font-medium text-gray-900">Successfully saved!</p>
// THE SESSION MESSAGE DISPLAYS HERE BUT NOT IN THE usePage(); ABOVE
<p class="mt-1 text-sm text-gray-500">{{ $page.props.flash.message }}</p>
</div>
<div class="ml-4 flex flex-shrink-0">
<button type="button" #click="show = false" class="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
<span class="sr-only">Close</span>
<XMarkIcon class="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
</div>
</transition>
</div>
</div>
</template>
Layout.vue
<template>
<div class="min-h-full">
<Notification></Notification>
<mobile-sidebar
:sidebarOpen="sidebarOpen"
#close="sidebarOpen = false"
></mobile-sidebar>
<!-- Static sidebar for desktop -->
<sidebar></sidebar>
<!-- Main column -->
<div class="flex flex-col lg:pl-64">
<mobile-header #sidebarOpen="sidebarOpen = true"></mobile-header>
<main class="flex-1">
<slot></slot>
</main>
</div>
</div>
</template>
<script setup>
import { ref, watch, onUpdated } from "vue";
import MobileHeader from "#/Shared/MobileHeader";
import MobileSidebar from "#/Shared/MobileSidebar";
import Sidebar from "#/Shared/Sidebar.vue";
import Alert from "#/Shared/Alert.vue";
import { usePage } from '#inertiajs/vue3'
const sidebarOpen = ref(false);
</script>
Help to list recursive with checkbox component data into parent component update values:
Parent File:
<TreeVue :items="props.allModules" :selectedItems="userForm.userModules"/>
TreeVue File:
<template>
<div v-for="item in items" :key="item.name" class="overflow-hidden rounded-lg bg-gray-50">
<div class="px-4 py-5 sm:p-6" >
<div class="flex items-center ">
<input :id="item.id" type="checkbox" :value="item.id" v-model="props.selectedItems" class="w-6 h-6 text-blue-600 bg-gray-100 rounded border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300">{{ item.name }}</label>
</div>
<div v-if="item.children" class="ml-5">
<Tree :items="item.children" :selectedItems="selectedItems" />
</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from "vue";
const props = defineProps({
items: Object,
selectedItems: Object,
});
</script>
ConsoleLog:
app.js:7839 [Vue warn] Set operation on key "selectedItems" failed: target is readonly. Proxy {items: Proxy, selectedItems: Proxy}
I need to update parent value after selected item on child TreeVue.
I read something about Emit, but I don't know how to implement it, because I'm new to Vue.
So I have made a custom card in Laravel nova and I want to pass in a URL as metadata and set the href attribute of a link to that URL.
It works but Vue keeps setting the URL to be an internal URL:
I want it to be :
http://foo.peoplebase.test/admin
but when I click the link I end up with is:
http://peoplebase.test/admin/dashboards/http//:foo.peoplebase.test
Here is the Vue file
<template>
<card class="flex flex-col items-center justify-center">
<div class="px-3 py-3">
<h1 class="text-center text-3xl text-80 font-light">{{ card.title }}</h1>
</div>
<div class="px-3 py-3">
<a :href="'http//:' + card.domain + '.peoplebase.test'" class="btn btn-default btn-primary inline-flex items-center relative" target="_blank">Website</a>
<a :href="'http//:' + card.domain + '.peoplebase.test/admin'" class="btn btn-default btn-primary inline-flex items-center relative" target="_blank">Backend</a>
Settings
</div>
</card>
</template>
<script>
export default {
props: [
'card',
// The following props are only available on resource detail cards...
// 'resource',
// 'resourceId',
// 'resourceName',
],
mounted() {
//
},
}
</script>
The normal href on the third link works fine but the dynaimc :href does not.
What am I missing here?
You have a typo! that's why it's not working as expected!
<a :href="'http//:'
change the http//: to http://
<a :href="'http://'
From what I've seen it should close without adding anything special but every time I click on another expandable item the previous one doesn't close.
<template>
<div d-flex p-0 m-0>
<div mb-3>
<div v-for="item in items" :key="item.Id">
<div v-if="item.HasChildren === true">
<div
class="scanList pa-2 font-weight-light"
v-ripple="{ class: 'success--text' }"
#click="swapComponent(item)"
>
{{ item.Name }}
</div>
</div>
<div v-else-if="item.HasChildren === false">
<v-expansion-panel>
<v-expansion-panel-content>
<template v-slot:header>
<div>{{ item.Name }}</div>
</template>
<v-card>
<QR justify-center :value="item.Id"></QR>
</v-card>
</v-expansion-panel-content>
</v-expansion-panel>
</div>
</div>
<div v-if="model != null && model.HasChildren === false">
<v-card>
<template v-slot:header>
<div>{{ item.FullPathName }}</div>
</template>
<QR justify-center :value="model.Id"></QR>
</v-card>
</div>
</div>
<div v-if="items !== initialItems">
<div class="backButton" #click="swapPrevious()">
<v-icon class="arrow">fa-arrow-left</v-icon>
</div>
</div>
</div>
</template>
I'm on Vuetify 1.5. Thanks for the help.
You are trying to create a separate expansion panel in the loop and its independent, explicitly we can define a logic to close the other panels
Working codepen here (using vuetify 1.5x)
Changes for HTML:
Added a event trigger on wrapper around expansion panel.
Added v-model for each expansion panel.
<div v-else-if="item.HasChildren === false" #click="closeOtherPanel(item.Id)">
<v-expansion-panel v-model="panel[item.Id]">
<v-expansion-panel-content>
<template v-slot:header>
<div>{{ item.Name }}</div>
</template>
<v-card>
<QR justify-center :value="item.Id"></QR>
</v-card>
</v-expansion-panel-content>
</v-expansion-panel>
</div>
Changes for script:
Add panel property inside data object.
Add a method to close other panels
data: {
panel: {},
},
methods: {
closeOtherPanel(id) {
var self = this;
Object.keys(this.panel).map(x => {
if (x != id) self.panel[x] = null;
});
}
}
Is it possible to use the built in Laravel Nova confirm dialogue in your own tool? All I would like to use is interact with it how Nova does itself.
The docs are quite light on the JS topic, as the only built in UI you seem to be able to work with is the toasted plugin: https://nova.laravel.com/docs/1.0/customization/frontend.html#javascript
You can use <modal> component whenever you want.
Here is how it work internally in Nova:
<template>
<modal
data-testid="confirm-action-modal"
tabindex="-1"
role="dialog"
#modal-close="handleClose"
class-whitelist="flatpickr-calendar"
>
<form
autocomplete="off"
#keydown="handleKeydown"
#submit.prevent.stop="handleConfirm"
class="bg-white rounded-lg shadow-lg overflow-hidden"
:class="{
'w-action-fields': action.fields.length > 0,
'w-action': action.fields.length == 0,
}"
>
<div>
<heading :level="2" class="border-b border-40 py-8 px-8">{{ action.name }}</heading>
<p v-if="action.fields.length == 0" class="text-80 px-8 my-8">
{{ __('Are you sure you want to run this action?') }}
</p>
<div v-else>
<!-- Validation Errors -->
<validation-errors :errors="errors" />
<!-- Action Fields -->
<div class="action" v-for="field in action.fields" :key="field.attribute">
<component
:is="'form-' + field.component"
:errors="errors"
:resource-name="resourceName"
:field="field"
/>
</div>
</div>
</div>
<div class="bg-30 px-6 py-3 flex">
<div class="flex items-center ml-auto">
<button
dusk="cancel-action-button"
type="button"
#click.prevent="handleClose"
class="btn text-80 font-normal h-9 px-3 mr-3 btn-link"
>
{{ __('Cancel') }}
</button>
<button
ref="runButton"
dusk="confirm-action-button"
:disabled="working"
type="submit"
class="btn btn-default"
:class="{
'btn-primary': !action.destructive,
'btn-danger': action.destructive,
}"
>
<loader v-if="working" width="30"></loader>
<span v-else>{{ __('Run Action') }}</span>
</button>
</div>
</div>
</form>
</modal>
</template>
<script>
export default {
props: {
working: Boolean,
resourceName: { type: String, required: true },
action: { type: Object, required: true },
selectedResources: { type: [Array, String], required: true },
errors: { type: Object, required: true },
},
/**
* Mount the component.
*/
mounted() {
// If the modal has inputs, let's highlight the first one, otherwise
// let's highlight the submit button
if (document.querySelectorAll('.modal input').length) {
document.querySelectorAll('.modal input')[0].focus()
} else {
this.$refs.runButton.focus()
}
},
methods: {
/**
* Stop propogation of input events unless it's for an escape or enter keypress
*/
handleKeydown(e) {
if (['Escape', 'Enter'].indexOf(e.key) !== -1) {
return
}
e.stopPropagation()
},
/**
* Execute the selected action.
*/
handleConfirm() {
this.$emit('confirm')
},
/**
* Close the modal.
*/
handleClose() {
this.$emit('close')
},
},
}
</script>
Here is simplified example:
<modal>
<form
autocomplete="off"
class="bg-white rounded-lg shadow-lg overflow-hidden"
>
<div>
<heading :level="2" class="border-b border-40 py-8 px-8">test</heading>
test
</div>
<div class="bg-30 px-6 py-3 flex">
<div class="flex items-center ml-auto">
<button
type="button"
class="btn text-80 font-normal h-9 px-3 mr-3 btn-link"
>
{{ __('Cancel') }}
</button>
<button
ref="runButton"
type="submit"
class="btn-danger"
>
<span>{{ __('Run Action') }}</span>
</button>
</div>
</div>
</form>
</modal>
You need to create a new component in the same folder of Tool.vue
I'll attach the component I used here
Then in the "handleConfirm" method, you can add a Ajax call to API
You can add you logic in that API.
You can find the API file in path, ToolName/routes/api.php
/* CustomModal.vue */
<template>
<modal tabindex="-1" role="dialog" #modal-close="handleClose">
<form #keydown="handleKeydown" #submit.prevent.stop="handleConfirm" class="bg-white rounded-lg shadow-lg overflow-hidden w-action">
<div>
<heading :level="2" class="border-b border-40 py-8 px-8">Confirm action</heading>
<p class="text-80 px-8 my-8"> Are you sure you want to perform this action? </p>
</div>
<div class="bg-30 px-6 py-3 flex">
<div class="flex items-center ml-auto">
<button type="button" #click.prevent="handleClose" class="btn btn-link dim cursor-pointer text-80 ml-auto mr-6">
Cancel
</button>
<button :disabled="working" type="submit" class="btn btn-default btn-primary">
<loader v-if="working" width="30"></loader>
<span v-else>Confirm</span>
</button>
</div>
</div>
</form>
</modal>
</template>
<script>
export default {
methods: {
handleConfirm() {
// Here you can add an ajax call to API and you can add your logic there.
},
handleClose() {
// Logic to hide the component
},
},
}
</script>
For more detailed explanation : https://medium.com/vineeth-vijayan/how-to-use-confirm-dialogs-in-a-laravel-nova-tool-b16424ffee87