November 27, 2025
ttj
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Create Blog Post - Validation Demo</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>
body { font-family: Arial, sans-serif; padding: 20px; max-width:800px; margin:auto; }
label{ display:block; margin-top:12px; font-weight:600 }
input[type="text"], textarea, select { width:100%; padding:8px; margin-top:6px; box-sizing:border-box; }
.small { font-size:0.9rem; color:#555; }
.error { color:#b00020; font-size:0.9rem; margin-top:6px; }
.ok { color:green; font-size:0.9rem; margin-top:6px; }
button { margin-top:12px; padding:10px 16px; }
.counters { margin-top:6px; font-size:0.9rem; color:#333; }
</style>
</head>
<body>
<h2>Create a Blog Post</h2>
<p class="small">Fill in details below to publish your post.</p>
<form id="blogForm" novalidate>
<label for="title">Title</label>
<input id="title" name="title" type="text" maxlength="150" placeholder="Enter title" />
<div id="titleError" class="error" aria-live="polite"></div>
<label for="content">Content</label>
<textarea id="content" name="content" rows="8" maxlength="10000" placeholder="Write your post..."></textarea>
<div class="counters">
<span id="wordCount">Words: 0</span> | <span id="charCount">Characters: 0</span>
</div>
<div id="contentError" class="error" aria-live="polite"></div>
<label for="category">Category</label>
<select id="category" name="category">
<option value="">-- Select category --</option>
<option value="refrigerator">Refrigerator</option>
<option value="dishwasher">Dishwasher</option>
<option value="cleaning">Cleaning</option>
<!-- add your categories -->
</select>
<div id="categoryError" class="error" aria-live="polite"></div>
<button type="submit">Publish</button>
<div id="formMessage" role="status" style="margin-top:10px;"></div>
</form>
<script>
(function() {
// Configurable rules
const TITLE_MIN = 3;
const TITLE_MAX = 150;
const CONTENT_MIN_WORDS = 10;
const CONTENT_MAX_CHARS = 10000;
// Disallowed special characters (adjust as needed). Note: emojis are outside ASCII range and won't match here.
// We purposely do NOT block unicode emoji ranges here.
const DISALLOWED_CHARS_REGEX = /[<>\\{}[\]^%$#@*+=~`|]/;
const form = document.getElementById('blogForm');
const titleEl = document.getElementById('title');
const contentEl = document.getElementById('content');
const categoryEl = document.getElementById('category');
const titleError = document.getElementById('titleError');
const contentError = document.getElementById('contentError');
const categoryError = document.getElementById('categoryError');
const formMessage = document.getElementById('formMessage');
const wordCountEl = document.getElementById('wordCount');
const charCountEl = document.getElementById('charCount');
// Utility: count words (splits on whitespace, ignores empty tokens)
function countWords(text) {
return (text.trim().split(/\s+/).filter(Boolean)).length;
}
// Basic client-side sanitization for display/preview; server must re-sanitize.
function sanitizeForSubmission(text) {
// replace angle brackets to avoid accidental HTML injection on client-side preview
return text.replace(/</g, "<").replace(/>/g, ">");
}
// Validate title
function validateTitle() {
const v = titleEl.value.trim();
titleError.textContent = '';
if (!v) {
titleError.textContent = 'Title is required.';
return false;
}
if (v.length < TITLE_MIN) {
titleError.textContent = `Title must be at least ${TITLE_MIN} characters.`;
return false;
}
if (v.length > TITLE_MAX) {
titleError.textContent = `Title must not exceed ${TITLE_MAX} characters.`;
return false;
}
if (DISALLOWED_CHARS_REGEX.test(v)) {
titleError.textContent = 'Title contains invalid characters.';
return false;
}
return true;
}
// Validate content
function validateContent() {
const v = contentEl.value;
contentError.textContent = '';
const words = countWords(v);
const chars = v.length;
if (!v.trim()) {
contentError.textContent = 'Content is required.';
return false;
}
if (words < CONTENT_MIN_WORDS) {
contentError.textContent = `Content must have at least ${CONTENT_MIN_WORDS} words (current: ${words}).`;
return false;
}
if (chars > CONTENT_MAX_CHARS) {
contentError.textContent = `Content must not exceed ${CONTENT_MAX_CHARS} characters.`;
return false;
}
if (DISALLOWED_CHARS_REGEX.test(v)) {
contentError.textContent = 'Content contains invalid characters.';
return false;
}
return true;
}
// Validate category
function validateCategory() {
categoryError.textContent = '';
if (!categoryEl.value) {
categoryError.textContent = 'Please select a category.';
return false;
}
return true;
}
// Live counters
function updateCounters() {
const txt = contentEl.value;
wordCountEl.textContent = 'Words: ' + countWords(txt);
charCountEl.textContent = 'Characters: ' + txt.length;
}
// Hook events
titleEl.addEventListener('input', () => {
titleError.textContent = '';
});
contentEl.addEventListener('input', () => {
contentError.textContent = '';
updateCounters();
});
categoryEl.addEventListener('change', () => {
categoryError.textContent = '';
});
// On submit
form.addEventListener('submit', function(e) {
e.preventDefault();
formMessage.textContent = '';
formMessage.className = '';
const okTitle = validateTitle();
const okContent = validateContent();
const okCategory = validateCategory();
if (!okTitle || !okContent || !okCategory) {
formMessage.textContent = 'Please fix the highlighted errors and try again.';
formMessage.className = 'error';
return;
}
// Prepare data for sending (example)
const payload = {
title: sanitizeForSubmission(titleEl.value.trim()),
content: sanitizeForSubmission(contentEl.value.trim()),
category: categoryEl.value
};
// Example: show success and simulated submit (replace with actual fetch/AJAX)
formMessage.textContent = 'Validation passed. Ready to submit.';
formMessage.className = 'ok';
// TODO: send payload to server using fetch/ajax and handle server-side validation/response
// fetch('/api/posts', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) })
// .then(r => r.json()).then(...)
console.log('Payload ready to submit:', payload);
});
// initialize counters
updateCounters();
// Optional: block paste of disallowed chars (UX improvement)
contentEl.addEventListener('paste', function(ev) {
// let paste data through, but we can sanitize or warn after paste in validateContent
setTimeout(updateCounters, 50);
});
})();
</script>
</body>
</html>
Log in to comment.