-
Notifications
You must be signed in to change notification settings - Fork 25
Tips for Developers
This is a quick-start guide for phpIP frontend development. For comprehensive documentation, see FRONTEND-ARCHITECTURE.md.
phpIP follows a minimal dependency, native JavaScript approach:
- No jQuery or heavy frameworks
- Lightweight vanilla JavaScript
- Data-driven configuration via HTML5 attributes
- Bootstrap 5 for UI components only
Fields that auto-save changes to the server on blur/change:
<tr data-resource="/event/123">
<td>
<input type="text" class="form-control noformat"
name="event_date" value="2024-01-15">
</td>
<td>
<input type="checkbox" class="noformat"
name="done" checked>
</td>
</tr>
Behavior:
- Input → Border turns blue
- Change → Auto-saves to server via PUT
- Success → Border turns green
- Error → Border turns red
Also works with contenteditable:
<dd data-resource="/classifier/789"
data-name="value"
contenteditable>
Patent Title
</dd>
Provides type-ahead suggestions from the server:
<!-- Hidden field stores the ID -->
<input type="hidden" name="actor_id">
<!-- Visible field shows the name -->
<input type="text"
data-ac="/actor/autocomplete/1"
data-actarget="actor_id"
placeholder="Actor Name">
Controller must return JSON:
[
{"key": "123", "value": "John Doe"},
{"key": "456", "value": "Jane Smith"}
]
Special keys:
-
key: 'create'
- Triggers create-on-the-fly logic
Single reusable modal that loads content dynamically:
<a href="/matter/123/events"
data-bs-toggle="modal"
data-bs-target="#ajaxModal"
data-size="modal-lg"
title="Event History">
View Events
</a>
Modal lifecycle:
- Click trigger → Fetches content from
href
- Sets modal title from
title
attribute - Applies size from
data-size
(optional) - Loads HTML into modal body
Located in public/js/main.js
:
Performs AJAX with automatic CSRF token handling:
// Create
fetchREST('/event', 'POST', new FormData(myForm))
.then(data => { /* handle response */ });
// Update
const params = new URLSearchParams();
params.append('field', 'value');
fetchREST('/resource/123', 'PUT', params)
.then(data => { /* handle response */ });
// Delete
fetchREST('/resource/123', 'DELETE')
.then(data => { /* handle response */ });
Refreshes a specific page section:
reloadPart(window.location.href, 'actorPanel');
Standardized modal form submission:
submitModalForm('/event', addEventForm, 'reloadModal', e.target);
Attribute | Purpose | Example |
---|---|---|
data-resource |
REST endpoint for resource | data-resource="/task/123" |
data-ac |
Autocomplete URL | data-ac="/actor/autocomplete" |
data-actarget |
Hidden input to populate | data-actarget="actor_id" |
data-aclength |
Min chars before trigger | data-aclength="2" |
data-bs-toggle |
Bootstrap modal trigger | data-bs-toggle="modal" |
data-bs-target |
Modal selector | data-bs-target="#ajaxModal" |
data-size |
Modal size class | data-size="modal-lg" |
data-panel |
Side panel selector | data-panel="ajaxPanel" |
<a data-bs-toggle="collapse" href="#addEventRow">+ Add Event</a>
<tr id="addEventRow" class="collapse">
<td colspan="5">
<form id="addEventForm">
<input type="hidden" name="matter_id" value="123">
<input type="text" name="event_date">
<button type="button" id="addEventSubmit">✓</button>
</form>
</td>
</tr>
<script>
document.getElementById('addEventSubmit').onclick = () => {
fetchREST('/event', 'POST', new FormData(addEventForm))
.then(data => {
if (!data.errors) {
reloadPart(window.location.href, 'eventList');
}
});
};
</script>
<table>
<tbody>
@foreach ($tasks as $task)
<tr data-resource="/task/{{ $task->id }}">
<td>
<input class="form-control noformat"
name="due_date"
value="{{ $task->due_date }}">
</td>
<td>
<input class="noformat"
type="checkbox"
name="done"
{{ $task->done ? 'checked' : '' }}>
</td>
</tr>
@endforeach
</tbody>
</table>
<form id="addActorForm">
<input type="hidden" name="actor_id">
<input type="text" id="actorName"
data-ac="/actor/autocomplete/1"
data-actarget="actor_id">
<button type="button" id="submitActor">Add</button>
</form>
<script>
document.getElementById('actorName').addEventListener('acCompleted', event => {
const selectedItem = event.detail;
if (selectedItem.key === 'create') {
// Create new actor on the fly
fetchREST('/actor', 'POST',
new URLSearchParams('name=' + selectedItem.value))
.then(response => {
addActorForm.actor_id.value = response.id;
});
}
});
document.getElementById('submitActor').onclick = () => {
fetchREST('/actor-pivot', 'POST', new FormData(addActorForm))
.then(data => {
if (!data.errors) {
reloadPart(window.location.href, 'actorPanel');
}
});
};
</script>
acCompleted
- Fired when autocomplete selection is made:
input.addEventListener('acCompleted', event => {
const selectedItem = event.detail; // {key, value, label, ...}
console.log('Selected:', selectedItem.key);
});
xhrsent
- Fired after successful inline edit:
ajaxPanel.addEventListener('xhrsent', () => {
refreshList(); // Update the list view
});
All interactions use event delegation on #app
element:
- Works with dynamically added content
- Single listener per event type
- Clean separation of concerns
- Use existing patterns from this guide
- Leverage
data-
attributes for configuration - Use event delegation (attach to
#app
) - Follow inline edit pattern for editable fields
- Reuse
#ajaxModal
for dialogs
# Install dependencies
composer install
yarn install
# Development (with hot reload)
yarn dev
# Production build
yarn build
# Run tests
./vendor/bin/phpunit
-
public/js/main.js
- Core helpers and global event listeners -
public/js/tables.js
- List filtering functionality -
public/js/matter-show.js
- Matter detail page interactions -
public/js/[view]-index.js
- View-specific list functionality -
resources/js/app.js
- Vite entry point (imports Bootstrap, Sass) -
resources/sass/app.scss
- Global styles
✅ DO:
- Use
.noformat
for inline editing - Configure via
data-
attributes - Reuse
#ajaxModal
for all modals - Use
fetchREST()
for AJAX operations - Use
reloadPart()
for partial updates - Follow existing patterns
❌ DON'T:
- Add jQuery or heavy frameworks
- Create new modals (use
#ajaxModal
) - Use
fetch()
directly (usefetchREST()
) - Reload full page when partial update works
- Break inline editing system
- Skip CSRF token handling
$validated = $request->validate([
'field' => 'required|string|max:255'
]);
// Laravel automatically returns on failure:
// {"errors": {"field": ["Error message"]}, "message": "..."}
fetchREST('/resource', 'POST', formData)
.then(data => {
if (data.errors) {
// Errors handled automatically by processSubmitErrors()
// Or manually:
Object.entries(data.errors).forEach(([field, messages]) => {
alert(`${field}: ${messages[0]}`);
});
} else {
// Success - redirect or update UI
reloadPart(window.location.href, 'panelId');
}
});
- Check browser console for errors
- Verify
href
attribute contains valid route - Ensure controller returns HTML (not JSON)
- Check
data-resource
exists on parent<tr>
or<div>
- Verify field has
.noformat
class - Confirm route accepts PUT requests
- Check browser console for CSRF errors
- Verify
data-ac
URL returns JSON array - Check JSON has
key
andvalue
fields - Ensure
data-actarget
matches hidden input name - Look for JavaScript errors in console
This guide covers the essentials. For comprehensive technical reference, including:
- Detailed implementation guides
- Troubleshooting scenarios
- Alpine.js integration guide
- Performance considerations
- Testing strategies
- Architecture diagrams
See FRONTEND-ARCHITECTURE.md in the /docs
directory.
When adding frontend features:
- Maintain native JavaScript approach
- Use existing patterns from this guide
- Add inline comments for complex logic
- Test with read-only user permissions
- Verify CSRF protection works
- Update documentation if adding new patterns
- GitHub Issues: https://github.com/jjdejong/phpip/issues
- Wiki: https://github.com/jjdejong/phpip/wiki
-
Detailed Docs:
/docs/FRONTEND-ARCHITECTURE.md