Skip to content

Tips for Developers

Jean J. de Jong edited this page Oct 1, 2025 · 16 revisions

Tips for Developers

This is a quick-start guide for phpIP frontend development. For comprehensive documentation, see FRONTEND-ARCHITECTURE.md.

Design Philosophy

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

Three Core Patterns

1. Inline Editing with .noformat

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>

2. Autocomplete with data-ac

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

3. AJAX Modal with #ajaxModal

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:

  1. Click trigger → Fetches content from href
  2. Sets modal title from title attribute
  3. Applies size from data-size (optional)
  4. Loads HTML into modal body

Core Helper Functions

Located in public/js/main.js:

fetchREST(url, method, body)

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 */ });

reloadPart(url, partId)

Refreshes a specific page section:

reloadPart(window.location.href, 'actorPanel');

submitModalForm(target, form, after, submitButton)

Standardized modal form submission:

submitModalForm('/event', addEventForm, 'reloadModal', e.target);

Data Attributes Reference

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"

Common Patterns

Pattern: Collapsible Add Form

<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>

Pattern: Table with Inline Editing

<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>

Pattern: Autocomplete with Dynamic Action

<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>

Event System

Custom Events

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
});

Global Event Delegation

All interactions use event delegation on #app element:

  • Works with dynamically added content
  • Single listener per event type
  • Clean separation of concerns

Development Workflow

1. Adding a New Feature

  1. Use existing patterns from this guide
  2. Leverage data- attributes for configuration
  3. Use event delegation (attach to #app)
  4. Follow inline edit pattern for editable fields
  5. Reuse #ajaxModal for dialogs

2. Build Process

# Install dependencies
composer install
yarn install

# Development (with hot reload)
yarn dev

# Production build
yarn build

# Run tests
./vendor/bin/phpunit

3. File Structure

  • 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

Best Practices

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 (use fetchREST())
  • Reload full page when partial update works
  • Break inline editing system
  • Skip CSRF token handling

Validation

Server-Side (Controller)

$validated = $request->validate([
    'field' => 'required|string|max:255'
]);

// Laravel automatically returns on failure:
// {"errors": {"field": ["Error message"]}, "message": "..."}

Client-Side Handling

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');
    }
  });

Troubleshooting

Modal not loading content?

  • Check browser console for errors
  • Verify href attribute contains valid route
  • Ensure controller returns HTML (not JSON)

Inline edit not saving?

  • 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

Autocomplete not working?

  • Verify data-ac URL returns JSON array
  • Check JSON has key and value fields
  • Ensure data-actarget matches hidden input name
  • Look for JavaScript errors in console

Complete Documentation

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.

Contributing

When adding frontend features:

  1. Maintain native JavaScript approach
  2. Use existing patterns from this guide
  3. Add inline comments for complex logic
  4. Test with read-only user permissions
  5. Verify CSRF protection works
  6. Update documentation if adding new patterns

Questions?

Clone this wiki locally