Understand the fundamental differences between JavaScript customization in Classic and Next-Gen Knack applications, including migration strategies and side-by-side code comparisons.
COMING SOON
What You'll Learn
You'll understand the fundamental architectural differences between Classic and Next-Gen Knack and learn how to completely start writing your customizations using Next-Gen custom code. This requires a shift in thinking from DOM manipulation to separation of concerns, but results in more maintainable applications.
Overview
The transition from Classic to Next-Gen Knack represents a shift in JavaScript customization architecture. Classic applications use jQuery and traditional DOM manipulation patterns, while Next-Gen introduces a React-based system with vanilla (aka native) JavaScript patterns, different event handling, and a virtual DOM.
Important: Each Knack version maintains separate custom code. Changes in one version are completely isolated from the other. Next-Gen uses React's virtual DOM system, which affects how DOM interactions work.
Key Architectural Differences
Classic Foundation: Traditional web technologies with jQuery-based DOM manipulation and immediate element access.
Next-Gen Foundation: React-integrated system with vanilla JavaScript patterns, virtual DOM considerations, and the global Knack object API.
Every JavaScript customization must begin with the
Knack.ready()
method. This ensures the Knack object is fully initialized and the React application has finished mounting before your custom code executes.
React maintains a virtual representation of the DOM and expects to control all structural changes. Moving, removing, or restructuring DOM elements will break React's unmount process and can cause unexpected behavior or errors.
Understanding the Fundamental Differences
Classic Knack used jQuery and traditional DOM manipulation, while Next-Gen uses React with a virtual DOM system. This requires rethinking your approach:
-
Working with the DOM Directly modifying the Document Object Model (DOM) is generally discouraged. Our platform is built on React, which uses a virtual DOM to efficiently manage updates to the user interface. When you manually change the DOM, React may not be aware of those changes. As a result, your edits may be overwritten, ignored, or cause unexpected side effects when React re-renders the component tree.
Some lightweight manipulations, such as disabling an input, updating button text, or hiding an element - may work without issue. But as the complexity of your changes increases, so does the risk of breaking functionality or creating inconsistencies between the DOM and React’s virtual DOM.
-
Events for logic - JavaScript handles business logic, not presentation
-
CSS-first approach - Apply styling through CSS selectors and classes
-
Official class names - Use documented Next-Gen CSS classes
Event Name Mapping
Event Translation Table
Classic Event | Next-Gen Event | Notes |
---|---|---|
knack-scene-render.scene_X | page:render:scene_X | Page-level rendering |
knack-view-render.view_X | view:render:view_X | Element-level rendering |
knack-form-submit.view_X | form:submit:view_X | Form submission |
knack-record-create.view_X | record:create | Record creation |
knack-record-update.view_X | record:update | Record updates |
knack-records-render.view_X | records:render:view_X | Table data rendering |
Event System Example
Classic Pattern (⚠️ )
// Classic event handling with DOM manipulation
$(document).on('knack-scene-render.scene_123', function(event, scene) {
$('#view_456 .kn-submit button').html('Save Contact');
$('#view_456 input[name="field_1"]').val('Default Value');
$('#view_456').css('background-color', '#f8f9fa');
});
$(document).on('knack-view-render.view_456', function(event, view) {
// More DOM manipulation
$('#view_456 .kn-button').css('color', 'red');
$('#view_456 form').addClass('custom-form');
});
Next-Gen Equivalent (✅ Use this)
CSS for visual changes:
/* Change button text and styling */
.scene_123_view_456 .kn-action-button::before {
content: 'Save Contact';
}
.scene_123_view_456 .kn-action-button span {
display: none;
}
.scene_123_view_456 .kn-action-button {
background-color: #28a745;
color: white;
padding: 12px 24px;
border-radius: 6px;
}
/* Style the view container */
.scene_123_view_456 {
background-color: #f8f9fa;
border-left: 3px solid #007bff;
border-radius: 8px;
padding: 1rem;
}
/* Style form inputs */
.scene_123_view_456 input[name*="field_1"] {
background-color: #e9ecef;
border: 2px solid #007bff;
}
JavaScript for logic:
Knack.ready().then(async () => {
Knack.on('page:render:scene_123', ({ pageKey }) => {
console.log('Dashboard page loaded');
// Business logic only
loadDashboardData();
trackPageAccess(pageKey);
});
Knack.on('view:render:view_456', ({ viewKey }) => {
console.log('Contact form loaded');
// Business logic only
initializeFormValidation();
loadContactDefaults();
});
async function loadDashboardData() {
try {
const user = await Knack.getUser();
const tables = await Knack.getTables();
console.log('Dashboard data loaded for:', user?.email);
} catch (error) {
console.error('Failed to load dashboard data:', error);
}
}
function trackPageAccess(pageKey) {
console.log('Page access tracked:', pageKey);
// Could send to analytics service
}
function loadContactDefaults() {
console.log('Contact defaults loaded');
// Could load default values from API
}
});
Example : Form Customization
Classic Code (⚠️ ):
$(document).on('knack-view-render.view_123', function(event, view) {
// Change button text
$('#view_123 .kn-submit button').html('Create Contact');
// Style the button
$('#view_123 .kn-submit button').css('background-color', '#28a745');
// Set default values
$('#view_123 input[name="field_1"]').val('Default Name');
// Add validation
$('#view_123 form').on('submit', function() {
var email = $('#view_123 input[name="field_email"]').val();
if (!email.includes('@')) {
alert('Invalid email');
return false;
}
});
// Style the form
$('#view_123').css({
'background-color': '#f8f9fa',
'border': '1px solid #dee2e6',
'border-radius': '8px',
'padding': '20px'
});
});
Next-Gen Code (✅ Use this):
CSS:
/* Style the form view */
.scene_123_view_456.kn-form-view {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
}
/* Style the action button */
.scene_123_view_456 .kn-action-button {
background-color: #28a745;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-weight: 600;
}
/* Change button text */
.scene_123_view_456 .kn-action-button::before {
content: 'Create Contact';
}
.scene_123_view_456 .kn-action-button span {
display: none;
}
/* Style form inputs */
.scene_123_view_456 input[name*="field_1"] {
background-color: #e9ecef;
border-left: 3px solid #28a745;
}
.scene_123_view_456 input[name*="field_email"] {
border: 2px solid #007bff;
}
.scene_123_view_456 input[name*="field_email"]:invalid {
border-color: #dc3545;
background-color: #f8d7da;
}
JavaScript:
Knack.ready().then(async () => {
Knack.on('view:render:view_456', ({ viewKey }) => {
console.log('Contact form rendered');
});
Knack.on('form:submit:view_456', ({ record, viewKey, isEdit }) => {
console.log('Contact form submitted:', data);
// Validation logic
if (data.field_email && !data.field_email.includes('@')) {
console.error('Invalid email format:', data.field_email);
// Could trigger error handling workflow
}
// Business logic
if (data.field_company) {
console.log('Company contact created:', data.field_company);
// Could trigger company-specific workflows
}
// Default value logic
if (!data.field_name) {
console.log('No name provided, using default workflow');
// Could trigger default name assignment
}
});
if (fieldKey === 'field_name' && !newValue) {
console.log('Name field cleared, applying default logic');
// Could trigger default value logic
}
});
});
Example: Table Customization
Classic Code (⚠️):
$(document).on('knack-view-render.view_789', function(event, view) {
// Style table headers
$('#view_789 .kn-table thead th').css({
'background-color': '#007bff',
'color': 'white',
'font-weight': 'bold'
});
// Add row highlighting
$('#view_789 .kn-table tbody tr').hover(
function() { $(this).css('background-color', '#f8f9fa'); },
function() { $(this).css('background-color', ''); }
);
// Color-code status column
$('#view_789 .kn-table tbody tr').each(function() {
var status = $(this).find('td:nth-child(3)').text();
if (status === 'Active') {
$(this).find('td:nth-child(3)').css('color', 'green');
} else if (status === 'Inactive') {
$(this).find('td:nth-child(3)').css('color', 'red');
}
});
});
Next-Gen Code (✅ Use this):
CSS:
/* Style table headers */
.scene_123_view_789 .kn-table-view th {
background-color: #007bff;
color: white;
font-weight: bold;
}
/* Row hover effects */
.scene_123_view_789 .kn-table-view tr:hover td {
background-color: #f8f9fa;
}
/* Status column styling using attribute selectors */
.scene_123_view_789 .kn-table-view td[data-field="field_status"] {
font-weight: bold;
text-align: center;
}
.scene_123_view_789 .kn-table-view td[data-field="field_status"][data-value="Active"] {
color: #28a745;
}
.scene_123_view_789 .kn-table-view td[data-field="field_status"][data-value="Inactive"] {
color: #dc3545;
}
/* Add status icons */
.scene_123_view_789 .kn-table-view td[data-field="field_status"][data-value="Active"]::before {
content: '✅ ';
}
.scene_123_view_789 .kn-table-view td[data-field="field_status"][data-value="Inactive"]::before {
content: '❌ ';
}
JavaScript:
Knack.ready().then(async () => {
Knack.on('records:render:view_789', ({ records, viewKey }) => {
console.log('Table data loaded:', records.length, 'records');
// Analyze data for business insights
const statusCounts = records.reduce((acc, record) => {
const status = record.field_status;
acc[status] = (acc[status] || 0) + 1;
return acc;
}, {});
console.log('Status distribution:', statusCounts);
// Business logic based on data
if (statusCounts.Inactive > statusCounts.Active) {
console.warn('More inactive than active records');
triggerStatusAlert(statusCounts);
} else {
// Remove alert if condition no longer met
document.body.classList.remove('status-alert-active');
}
});
function triggerStatusAlert(statusCounts) {
// Add CSS class to trigger visual alert
document.body.classList.add('status-alert-active');
// Log for debugging/analytics
console.warn('Status Alert Triggered:', statusCounts);
console.table(statusCounts);
// Could also send to external services
// sendToAnalytics('status_alert', statusCounts);
}
});
/* Status alert banner - hidden by default */
.status-alert-banner {
display: none;
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
border-radius: 4px;
padding: 1rem;
margin: 1rem 0;
font-weight: 500;
position: relative;
}
.status-alert-banner::before {
content: '⚠️ ';
font-size: 1.2em;
margin-right: 0.5rem;
}
/* Show alert when status-alert-active class is added to body */
.status-alert-active .status-alert-banner {
display: block;
animation: slideIn 0.3s ease-out;
}
/* Optional: Style the table view when alert is active */
.status-alert-active .scene_789_view_789 {
border-left: 4px solid #dc3545;
}
.status-alert-active .scene_789_view_789 .kn-table-view {
background-color: #fff5f5;
}
/* Animation for alert appearance */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Close button styling (optional) */
.status-alert-banner .close-btn {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: none;
border: none;
color: #721c24;
font-size: 1.2em;
cursor: pointer;
padding: 0.25rem;
border-radius: 2px;
}
.status-alert-banner .close-btn:hover {
background-color: rgba(0,0,0,0.1);
}
Common Patterns
jQuery Selectors → CSS Selectors
// ❌ Classic jQuery selectors
$('#view_123 .kn-button')
$('.kn-table tbody tr:nth-child(odd)')
$('input[name="field_1"]')
// ✅ Next-Gen CSS selectors
.scene_123_view_456 .kn-action-button
.kn-table-view tr:nth-child(odd) td
.kn-form-view input[name*="field_1"]
Event Handling Migration
// ❌ Classic jQuery events
$(document).on('knack-scene-render.scene_123', function() {});
$(document).on('knack-view-render.view_456', function() {});
$(document).on('knack-form-submit.view_456', function() {});
// ✅ Next-Gen Knack events
Knack.on('page:render:scene_123', ({ pageKey }) => {});
Knack.on('view:render:view_456', ({ viewKey }) => {});
Knack.on('form:submit:view_456', ({ record, viewKey, isEdit }) => {});
DOM Manipulation → CSS Classes
// ❌ Classic DOM manipulation
$('#view_123').css('background-color', 'red');
$('#view_123 .kn-button').addClass('custom-button');
// ✅ Next-Gen CSS approach
/* Use CSS file */
.scene_123_view_456 { background-color: red; }
.scene_123_view_456 .kn-action-button { /* custom styles */ }
/* Or add class in JavaScript for state changes */
document.body.classList.add('form-submitted');