$npx -y skills add lubusIN/frappe-skills --skill frappe-frontend-developmentBuild modern Vue 3 frontend apps using Frappe UI with components, data fetching, and portal pages. Use when creating custom frontends, SPAs, or portal interfaces for Frappe applications.
| 1 | # Frappe Frontend Development |
| 2 | |
| 3 | Build modern frontend applications using Frappe UI (Vue 3 + TailwindCSS) and portal pages. |
| 4 | |
| 5 | ## When to use |
| 6 | |
| 7 | - Building a custom SPA frontend for a Frappe app |
| 8 | - Using Frappe UI components (Button, Dialog, ListView, etc.) |
| 9 | - Implementing data fetching with Resource, ListResource, DocumentResource |
| 10 | - Creating portal/public-facing pages |
| 11 | - Setting up Vue 3 frontend tooling inside a Frappe app |
| 12 | |
| 13 | ## Inputs required |
| 14 | |
| 15 | - App name and whether frontend already exists |
| 16 | - Frontend type (full SPA via Frappe UI, or portal pages) |
| 17 | - Authentication requirements (logged-in users, guest access) |
| 18 | - Key components and data resources needed |
| 19 | |
| 20 | ## Procedure |
| 21 | |
| 22 | ### 0) Choose frontend approach |
| 23 | |
| 24 | | Approach | When to Use | Stack | |
| 25 | |----------|-------------|-------| |
| 26 | | Frappe UI SPA | Custom app frontend | Vue 3, TailwindCSS, Vite | |
| 27 | | Portal pages | Simple public pages | Jinja + HTML, minimal JS | |
| 28 | | Desk extensions | Admin UI enhancements | Form/List scripts (see `frappe-desk-customization`) | |
| 29 | |
| 30 | ### 1) Scaffold Frappe UI frontend |
| 31 | |
| 32 | ```bash |
| 33 | # Inside your Frappe app directory |
| 34 | cd apps/my_app |
| 35 | npx degit frappe/frappe-ui-starter frontend |
| 36 | |
| 37 | # Install dependencies |
| 38 | cd frontend |
| 39 | yarn |
| 40 | |
| 41 | # Start dev server |
| 42 | yarn dev |
| 43 | ``` |
| 44 | |
| 45 | ### 2) Configure main.js |
| 46 | |
| 47 | ```javascript |
| 48 | import { createApp } from 'vue' |
| 49 | import { |
| 50 | FrappeUI, |
| 51 | setConfig, |
| 52 | frappeRequest, |
| 53 | resourcesPlugin, |
| 54 | pageMetaPlugin |
| 55 | } from 'frappe-ui' |
| 56 | import App from './App.vue' |
| 57 | import './index.css' |
| 58 | |
| 59 | let app = createApp(App) |
| 60 | |
| 61 | // Register FrappeUI plugin (components + directives) |
| 62 | app.use(FrappeUI) |
| 63 | |
| 64 | // Enable Frappe response parsing |
| 65 | setConfig('resourceFetcher', frappeRequest) |
| 66 | |
| 67 | // Optional: Options API resource support |
| 68 | app.use(resourcesPlugin) |
| 69 | |
| 70 | // Optional: Reactive page titles |
| 71 | app.use(pageMetaPlugin) |
| 72 | |
| 73 | app.mount('#app') |
| 74 | ``` |
| 75 | |
| 76 | ### 3) Fetch data with Resources |
| 77 | |
| 78 | **Generic Resource** — for custom API calls: |
| 79 | |
| 80 | ```javascript |
| 81 | import { createResource } from 'frappe-ui' |
| 82 | |
| 83 | let stats = createResource({ |
| 84 | url: 'my_app.api.get_dashboard_stats', |
| 85 | params: { period: 'monthly' }, |
| 86 | auto: true, |
| 87 | cache: 'dashboard-stats', |
| 88 | transform(data) { |
| 89 | return { ...data, formatted_total: format_currency(data.total) } |
| 90 | }, |
| 91 | onSuccess(data) { console.log('Loaded:', data) }, |
| 92 | onError(error) { console.error('Failed:', error) } |
| 93 | }) |
| 94 | |
| 95 | // Properties |
| 96 | stats.data // Response data |
| 97 | stats.loading // Boolean: request in progress |
| 98 | stats.error // Error object if failed |
| 99 | stats.fetched // Boolean: data fetched at least once |
| 100 | |
| 101 | // Methods |
| 102 | stats.fetch() // Trigger request |
| 103 | stats.reload() // Re-fetch |
| 104 | stats.submit({ period: 'weekly' }) // Fetch with new params |
| 105 | stats.reset() // Reset state |
| 106 | ``` |
| 107 | |
| 108 | **List Resource** — for DocType lists with pagination: |
| 109 | |
| 110 | ```javascript |
| 111 | import { createListResource } from 'frappe-ui' |
| 112 | |
| 113 | let todos = createListResource({ |
| 114 | doctype: 'ToDo', |
| 115 | fields: ['name', 'description', 'status'], |
| 116 | filters: { status: 'Open' }, |
| 117 | orderBy: 'creation desc', |
| 118 | pageLength: 20, |
| 119 | auto: true, |
| 120 | cache: 'open-todos' |
| 121 | }) |
| 122 | |
| 123 | // List-specific API |
| 124 | todos.data // Array of records |
| 125 | todos.hasNextPage // Boolean: more pages |
| 126 | todos.next() // Load next page |
| 127 | todos.reload() // Refresh list |
| 128 | |
| 129 | // CRUD operations |
| 130 | todos.insert.submit({ description: 'New task' }) |
| 131 | todos.setValue.submit({ name: 'TODO-001', status: 'Closed' }) |
| 132 | todos.delete.submit('TODO-001') |
| 133 | todos.runDocMethod.submit({ method: 'send_email', name: 'TODO-001' }) |
| 134 | ``` |
| 135 | |
| 136 | **Document Resource** — for single document operations: |
| 137 | |
| 138 | ```javascript |
| 139 | import { createDocumentResource } from 'frappe-ui' |
| 140 | |
| 141 | let todo = createDocumentResource({ |
| 142 | doctype: 'ToDo', |
| 143 | name: 'TODO-001', |
| 144 | whitelistedMethods: { |
| 145 | sendEmail: 'send_email', |
| 146 | markComplete: 'mark_complete' |
| 147 | }, |
| 148 | onSuccess(doc) { console.log('Loaded:', doc.name) } |
| 149 | }) |
| 150 | |
| 151 | // Document API |
| 152 | todo.doc // Full document object |
| 153 | todo.reload() // Refresh document |
| 154 | |
| 155 | // Update fields |
| 156 | todo.setValue.submit({ status: 'Closed' }) |
| 157 | |
| 158 | // Debounced update (coalesces rapid changes) |
| 159 | todo.setValueDebounced.submit({ description: 'Updated' }) |
| 160 | |
| 161 | // Call whitelisted methods |
| 162 | todo.sendEmail.submit({ email: 'user@example.com' }) |
| 163 | |
| 164 | // Delete |
| 165 | todo.delete.submit() |
| 166 | ``` |
| 167 | |
| 168 | ### 4) Use Frappe UI components |
| 169 | |
| 170 | ```vue |
| 171 | <template> |
| 172 | <div class="p-4"> |
| 173 | <Button variant="solid" theme="blue" @click="showDialog = true"> |
| 174 | Add Todo |
| 175 | </Button> |
| 176 | |
| 177 | <ListView :columns="columns" :rows="todos.data"> |
| 178 | <template #cell="{ column, row, value }"> |
| 179 | <Badge v-if="column.key === 'status'" :theme="value === 'Open' ? 'orange' : 'green'"> |
| 180 | {{ value }} |
| 181 | </Badge> |
| 182 | <span v-else>{{ value }}</span> |
| 183 | </template> |
| 184 | </ListView> |
| 185 | |
| 186 | <Dia |