Question

i want to create a web app that allows the user to create tasks.
it has the following features:
* creation of tasks: each tasks has a title and a text field
* the tasks are displayed in a table view
* creation of custom fields: the user can create additional fields which can then be filled out in the task. the types are: single select, date, string, number. for single select the user can specify the entries.
* the custom fields are displayed in the table view.
* tasks can be deleted or closed
* task creation and custom field creation are in a popup
* you can filter the task list by text or by the value of a custom field

implementation:
* no react, just plain javascript
* the data dhould be stored locally, use an IndexedDB for storage
* use a single html file only, no external script files

Answer

Anlegen und verwalten

Ich überlege eine Web-App, die Aufgaben mit Titeln, Textfeldern und benutzerdefinierten Feldern erstellt, anzeigt und bearbeitet, lokal mit IndexedDB speichert.

Erstellen des Codes

Ich erarbeite einen HTML-Datei, der JavaScript für die Funktionalität und IndexedDB für Speicherung nutzt. Abschnittsweise strukturiert, inklusive Erklärungen.

Answer

Creating a web application with the specified features using plain JavaScript and IndexedDB is a comprehensive task. Below is a complete implementation contained within a single HTML file. This file includes all necessary HTML, CSS, and JavaScript code to achieve the desired functionality.

Overview

  1. HTML Structure: Defines the layout of the application, including the task table, buttons for creating tasks and custom fields, and popups for input forms.

  2. CSS Styling: Provides basic styling for the application to make it user-friendly and visually organized.

  3. JavaScript Functionality:

    • IndexedDB Setup: Initializes the database with two object stores: tasks and customFields.
    • Task Management: Functions to create, display, delete, and close tasks.
    • Custom Field Management: Functions to create custom fields and handle their specific data types.
    • Filtering: Allows users to filter tasks based on text input or custom field values.
    • Event Handling: Manages user interactions with buttons and forms.

Complete Code

Save the following code into an .html file (e.g., task_manager.html) and open it in your web browser to run the application.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Task Manager App</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } h1 { text-align: center; } #controls { margin-bottom: 20px; text-align: center; } #controls button { margin: 0 10px; padding: 10px 20px; } #filterSection { margin-bottom: 20px; text-align: center; } #filterSection input, #filterSection select { margin: 0 5px; padding: 5px; } table { width: 100%; border-collapse: collapse; margin-top: 10px; } th, td { border: 1px solid #ccc; padding: 8px; text-align: left; } th { background-color: #f4f4f4; } .action-btn { margin-right: 5px; padding: 5px 10px; } /* Popup styling */ .popup { display: none; position: fixed; z-index: 1; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4); } .popup-content { background-color: #fff; margin: 10% auto; padding: 20px; border: 1px solid #888; width: 50%; } .close { color: #aaa; float: right; font-size: 24px; font-weight: bold; cursor: pointer; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; } input[type="text"], textarea, select, input[type="number"], input[type="date"] { width: 100%; padding: 8px; box-sizing: border-box; } .custom-select-options { margin-top: 10px; } .custom-select-options input { width: 80%; margin-right: 5px; } .custom-select-options button { padding: 5px 10px; } </style> </head> <body> <h1>Task Manager App</h1> <div id="controls"> <button id="createTaskBtn">Create Task</button> <button id="createFieldBtn">Create Custom Field</button> </div> <div id="filterSection"> <input type="text" id="textFilter" placeholder="Filter by text"> <select id="fieldFilter"> <option value="">Filter by custom field</option> </select> <button id="applyFilterBtn">Apply Filter</button> <button id="clearFilterBtn">Clear Filter</button> </div> <table id="taskTable"> <thead> <tr id="tableHeader"> <th>Title</th> <th>Description</th> <th>Status</th> <th>Actions</th> <!-- Custom fields headers will be appended here --> </tr> </thead> <tbody id="taskTableBody"> <!-- Tasks will be populated here --> </tbody> </table> <!-- Popup for creating tasks --> <div id="taskPopup" class="popup"> <div class="popup-content"> <span class="close" id="closeTaskPopup">&times;</span> <h2>Create Task</h2> <form id="taskForm"> <div class="form-group"> <label for="taskTitle">Title:</label> <input type="text" id="taskTitle" required> </div> <div class="form-group"> <label for="taskDescription">Description:</label> <textarea id="taskDescription" rows="4" required></textarea> </div> <div id="customFieldsSection"> <!-- Dynamic custom fields will be appended here --> </div> <button type="submit">Create Task</button> </form> </div> </div> <!-- Popup for creating custom fields --> <div id="fieldPopup" class="popup"> <div class="popup-content"> <span class="close" id="closeFieldPopup">&times;</span> <h2>Create Custom Field</h2> <form id="fieldForm"> <div class="form-group"> <label for="fieldName">Field Name:</label> <input type="text" id="fieldName" required> </div> <div class="form-group"> <label for="fieldType">Field Type:</label> <select id="fieldType" required> <option value="">Select type</option> <option value="string">String</option> <option value="number">Number</option> <option value="date">Date</option> <option value="singleSelect">Single Select</option> </select> </div> <div class="form-group" id="singleSelectOptions" style="display: none;"> <label>Options for Single Select (comma separated):</label> <input type="text" id="singleSelectInput" placeholder="e.g., Option1, Option2"> </div> <button type="submit">Create Field</button> </form> </div> </div> <script> // Initialize IndexedDB let db; const request = indexedDB.open('TaskManagerDB', 1); request.onupgradeneeded = function(event) { db = event.target.result; const taskStore = db.createObjectStore('tasks', { keyPath: 'id', autoIncrement: true }); taskStore.createIndex('title', 'title', { unique: false }); const fieldStore = db.createObjectStore('customFields', { keyPath: 'id', autoIncrement: true }); fieldStore.createIndex('name', 'name', { unique: true }); }; request.onsuccess = function(event) { db = event.target.result; loadCustomFields(); loadTasks(); }; request.onerror = function(event) { console.error('Database error:', event.target.errorCode); }; // Utility function to get all custom fields function getAllCustomFields() { return new Promise((resolve, reject) => { const transaction = db.transaction(['customFields'], 'readonly'); const store = transaction.objectStore('customFields'); const request = store.getAll(); request.onsuccess = function() { resolve(request.result); }; request.onerror = function() { reject(request.error); }; }); } // Load custom fields and update the filter dropdown and task form async function loadCustomFields() { const customFields = await getAllCustomFields(); const fieldFilter = document.getElementById('fieldFilter'); // Clear existing options fieldFilter.innerHTML = '<option value="">Filter by custom field</option>'; customFields.forEach(field => { const option = document.createElement('option'); option.value = field.id; option.textContent = field.name; fieldFilter.appendChild(option); }); // Update task form with custom fields const customFieldsSection = document.getElementById('customFieldsSection'); customFieldsSection.innerHTML = '<h3>Custom Fields</h3>'; customFields.forEach(field => { const div = document.createElement('div'); div.className = 'form-group'; const label = document.createElement('label'); label.textContent = field.name + ':'; div.appendChild(label); let input; switch(field.type) { case 'string': input = document.createElement('input'); input.type = 'text'; break; case 'number': input = document.createElement('input'); input.type = 'number'; break; case 'date': input = document.createElement('input'); input.type = 'date'; break; case 'singleSelect': input = document.createElement('select'); const defaultOption = document.createElement('option'); defaultOption.value = ''; defaultOption.textContent = 'Select an option'; input.appendChild(defaultOption); field.options.forEach(opt => { const option = document.createElement('option'); option.value = opt; option.textContent = opt; input.appendChild(option); }); break; } input.id = 'custom_' + field.id; div.appendChild(input); customFieldsSection.appendChild(div); }); // Update table headers with custom fields const tableHeader = document.getElementById('tableHeader'); // Remove existing custom field headers const existingCustomHeaders = document.querySelectorAll('.custom-header'); existingCustomHeaders.forEach(th => th.remove()); // Add new custom field headers customFields.forEach(field => { const th = document.createElement('th'); th.textContent = field.name; th.className = 'custom-header'; tableHeader.insertBefore(th, tableHeader.lastElementChild); }); } // Load tasks from IndexedDB and display them async function loadTasks() { const transaction = db.transaction(['tasks', 'customFields'], 'readonly'); const taskStore = transaction.objectStore('tasks'); const request = taskStore.getAll(); request.onsuccess = async function() { const tasks = request.result; const customFields = await getAllCustomFields(); displayTasks(tasks, customFields); }; } // Display tasks in the table function displayTasks(tasks, customFields, filters = {}) { const tbody = document.getElementById('taskTableBody'); tbody.innerHTML = ''; tasks.forEach(task => { // Apply filters let show = true; if(filters.text) { const textMatch = task.title.toLowerCase().includes(filters.text.toLowerCase()) || task.description.toLowerCase().includes(filters.text.toLowerCase()); if(!textMatch) show = false; } if(filters.fieldId && filters.fieldValue) { if(!task.customFields || task.customFields[filters.fieldId] !== filters.fieldValue) { show = false; } } if(show) { const tr = document.createElement('tr'); const titleTd = document.createElement('td'); titleTd.textContent = task.title; tr.appendChild(titleTd); const descTd = document.createElement('td'); descTd.textContent = task.description; tr.appendChild(descTd); const statusTd = document.createElement('td'); statusTd.textContent = task.status || 'Open'; tr.appendChild(statusTd); // Add custom fields customFields.forEach(field => { const customTd = document.createElement('td'); customTd.textContent = task.customFields ? task.customFields[field.id] || '' : ''; tr.appendChild(customTd); }); const actionsTd = document.createElement('td'); // Close/Delete buttons if(task.status !== 'Closed') { const closeBtn = document.createElement('button'); closeBtn.textContent = 'Close'; closeBtn.className = 'action-btn'; closeBtn.onclick = () => closeTask(task.id); actionsTd.appendChild(closeBtn); } const deleteBtn = document.createElement('button'); deleteBtn.textContent = 'Delete'; deleteBtn.className = 'action-btn'; deleteBtn.onclick = () => deleteTask(task.id); actionsTd.appendChild(deleteBtn); tr.appendChild(actionsTd); tbody.appendChild(tr); } }); } // Create Task Popup const taskPopup = document.getElementById('taskPopup'); const createTaskBtn = document.getElementById('createTaskBtn'); const closeTaskPopup = document.getElementById('closeTaskPopup'); createTaskBtn.onclick = function() { taskPopup.style.display = 'block'; }; closeTaskPopup.onclick = function() { taskPopup.style.display = 'none'; document.getElementById('taskForm').reset(); }; window.onclick = function(event) { if(event.target == taskPopup) { taskPopup.style.display = 'none'; document.getElementById('taskForm').reset(); } if(event.target == fieldPopup) { fieldPopup.style.display = 'none'; document.getElementById('fieldForm').reset(); document.getElementById('singleSelectOptions').style.display = 'none'; } }; // Handle Task Form Submission const taskForm = document.getElementById('taskForm'); taskForm.onsubmit = function(e) { e.preventDefault(); const title = document.getElementById('taskTitle').value.trim(); const description = document.getElementById('taskDescription').value.trim(); if(!title || !description) { alert('Please fill in all required fields.'); return; } getAllCustomFields().then(customFields => { let customFieldsData = {}; customFields.forEach(field => { const value = document.getElementById('custom_' + field.id).value; if(value) { customFieldsData[field.id] = value; } }); const transaction = db.transaction(['tasks'], 'readwrite'); const store = transaction.objectStore('tasks'); const task = { title, description, status: 'Open', customFields: customFieldsData }; store.add(task).onsuccess = function() { loadTasks(); taskPopup.style.display = 'none'; taskForm.reset(); }; }); }; // Delete Task function deleteTask(id) { if(confirm('Are you sure you want to delete this task?')) { const transaction = db.transaction(['tasks'], 'readwrite'); const store = transaction.objectStore('tasks'); store.delete(id).onsuccess = function() { loadTasks(); }; } } // Close Task function closeTask(id) { const transaction = db.transaction(['tasks'], 'readwrite'); const store = transaction.objectStore('tasks'); const getRequest = store.get(id); getRequest.onsuccess = function() { const task = getRequest.result; task.status = 'Closed'; store.put(task).onsuccess = function() { loadTasks(); }; }; } // Create Custom Field Popup const fieldPopup = document.getElementById('fieldPopup'); const createFieldBtn = document.getElementById('createFieldBtn'); const closeFieldPopup = document.getElementById('closeFieldPopup'); const fieldTypeSelect = document.getElementById('fieldType'); createFieldBtn.onclick = function() { fieldPopup.style.display = 'block'; }; closeFieldPopup.onclick = function() { fieldPopup.style.display = 'none'; document.getElementById('fieldForm').reset(); document.getElementById('singleSelectOptions').style.display = 'none'; }; // Show/hide single select options based on field type fieldTypeSelect.onchange = function() { if(this.value === 'singleSelect') { document.getElementById('singleSelectOptions').style.display = 'block'; } else { document.getElementById('singleSelectOptions').style.display = 'none'; } }; // Handle Field Form Submission const fieldForm = document.getElementById('fieldForm'); fieldForm.onsubmit = function(e) { e.preventDefault(); const name = document.getElementById('fieldName').value.trim(); const type = document.getElementById('fieldType').value; if(!name || !type) { alert('Please fill in all required fields.'); return; } let options = []; if(type === 'singleSelect') { const optionsInput = document.getElementById('singleSelectInput').value.trim(); if(!optionsInput) { alert('Please provide options for single select.'); return; } options = optionsInput.split(',').map(opt => opt.trim()).filter(opt => opt); if(options.length === 0) { alert('Please provide valid options.'); return; } } const transaction = db.transaction(['customFields'], 'readwrite'); const store = transaction.objectStore('customFields'); const field = { name, type, options: type === 'singleSelect' ? options : [] }; store.add(field).onsuccess = function() { loadCustomFields(); loadTasks(); fieldPopup.style.display = 'none'; fieldForm.reset(); document.getElementById('singleSelectOptions').style.display = 'none'; }; store.onerror = function() { alert('Field name must be unique.'); }; }; // Filtering const applyFilterBtn = document.getElementById('applyFilterBtn'); const clearFilterBtn = document.getElementById('clearFilterBtn'); applyFilterBtn.onclick = async function() { const text = document.getElementById('textFilter').value.trim(); const fieldId = document.getElementById('fieldFilter').value; const fieldValue = fieldId ? prompt('Enter value for the selected custom field to filter:') : null; const filters = {}; if(text) filters.text = text; if(fieldId && fieldValue !== null) { filters.fieldId = parseInt(fieldId); filters.fieldValue = fieldValue.trim(); } const transaction = db.transaction(['tasks', 'customFields'], 'readonly'); const taskStore = transaction.objectStore('tasks'); const request = taskStore.getAll(); request.onsuccess = async function() { const tasks = request.result; const customFields = await getAllCustomFields(); displayTasks(tasks, customFields, filters); }; }; clearFilterBtn.onclick = function() { document.getElementById('textFilter').value = ''; document.getElementById('fieldFilter').value = ''; loadTasks(); }; </script> </body> </html>

Explanation of Key Components

1. IndexedDB Initialization

The application uses IndexedDB to store tasks and custom fields locally in the user's browser.

let db; const request = indexedDB.open('TaskManagerDB', 1); request.onupgradeneeded = function(event) { db = event.target.result; const taskStore = db.createObjectStore('tasks', { keyPath: 'id', autoIncrement: true }); taskStore.createIndex('title', 'title', { unique: false }); const fieldStore = db.createObjectStore('customFields', { keyPath: 'id', autoIncrement: true }); fieldStore.createIndex('name', 'name', { unique: true }); };
  • Object Stores:
    • tasks: Stores task objects with properties like title, description, status, and customFields.
    • customFields: Stores custom field definitions with name, type, and options (for single select).

2. Creating Custom Fields

Users can create custom fields of types: string, number, date, and single select. For single select fields, users provide the options.

fieldForm.onsubmit = function(e) { e.preventDefault(); const name = document.getElementById('fieldName').value.trim(); const type = document.getElementById('fieldType').value; // Validation and options handling const field = { name, type, options: type === 'singleSelect' ? options : [] }; // Add to IndexedDB };
  • The form collects the field name and type.
  • If the type is singleSelect, it also collects the available options.
  • The new field is then stored in the customFields object store.

3. Creating Tasks

Users can create tasks by providing a title, description, and filling out any custom fields.

taskForm.onsubmit = function(e) { e.preventDefault(); const title = document.getElementById('taskTitle').value.trim(); const description = document.getElementById('taskDescription').value.trim(); // Collect custom field values const task = { title, description, status: 'Open', customFields: customFieldsData }; // Add to IndexedDB };
  • The form collects the task title and description.
  • It dynamically includes input fields for each custom field.
  • The task, along with its custom field values, is stored in the tasks object store.

4. Displaying Tasks

Tasks are displayed in a table view, including any custom fields.

function displayTasks(tasks, customFields, filters = {}) { // Iterate over tasks and create table rows // Include title, description, status, custom fields, and action buttons }
  • The table headers dynamically include columns for each custom field.
  • Each task row includes the task details and action buttons (Close and Delete).

5. Filtering Tasks

Users can filter tasks based on text input or specific custom field values.

applyFilterBtn.onclick = async function() { const text = document.getElementById('textFilter').value.trim(); const fieldId = document.getElementById('fieldFilter').value; const fieldValue = fieldId ? prompt('Enter value for the selected custom field to filter:') : null; // Apply filters and display filtered tasks };
  • Text Filter: Filters tasks where the title or description includes the input text.
  • Custom Field Filter: Prompts the user to enter a value for the selected custom field and filters tasks accordingly.

6. Handling Popups

Popups are used for creating tasks and custom fields. They are displayed and hidden based on user interactions.

createTaskBtn.onclick = function() { taskPopup.style.display = 'block'; }; closeTaskPopup.onclick = function() { taskPopup.style.display = 'none'; document.getElementById('taskForm').reset(); };
  • Clicking the "Create Task" or "Create Custom Field" buttons opens the respective popup.
  • Clicking the close icon (&times;) or outside the popup closes it and resets the form.

Usage Instructions

  1. Create Custom Fields:

    • Click on the "Create Custom Field" button.
    • Fill in the field name and select the field type.
    • If selecting "Single Select," provide comma-separated options.
    • Submit the form to add the custom field.
  2. Create Tasks:

    • Click on the "Create Task" button.
    • Enter the task title and description.
    • Fill in any custom fields that have been created.
    • Submit the form to add the task to the table.
  3. Manage Tasks:

    • Close Task: Click the "Close" button to mark a task as closed.
    • Delete Task: Click the "Delete" button to remove a task permanently.
  4. Filter Tasks:

    • Use the text input to filter tasks by title or description.
    • Use the dropdown to select a custom field and provide a value to filter tasks based on that field.
    • Click "Apply Filter" to see the filtered tasks.
    • Click "Clear Filter" to remove all filters and view all tasks.

Notes

  • Data Persistence: The application uses IndexedDB to store data locally in the browser. Clearing the browser data or using a different browser/device will result in loss of stored tasks and custom fields.

  • Unique Field Names: Custom field names must be unique. Attempting to create a field with an existing name will result in an error.

  • Single HTML File: The entire application is contained within a single HTML file with embedded CSS and JavaScript, adhering to the implementation requirements.

  • Browser Compatibility: Ensure that your browser supports IndexedDB. Most modern browsers do, but older versions might not.

Feel free to customize and enhance the application further based on your specific needs!

Share this Q&A