State Management and Saving Settings - WazeDev/WazeDev.github.io GitHub Wiki
As your WME scripts grow more complex, you'll want to give users control over how they behave. Most scripts include settings that users can toggle on/off, configure thresholds, or customize behavior. Without proper state management, users would have to reconfigure their preferences every time they reload WME - a poor user experience.
In this section, we'll build a robust settings system that:
- Persists user preferences between browser sessions using localStorage
- Provides immediate feedback when settings change
- Scales easily as you add more options to your script
- Handles errors gracefully when storage isn't available
We'll start by adding a simple "Enable/Disable" checkbox to give users control over whether the script is active. This is a common pattern in WME scripts - allowing users to quickly toggle functionality without having to disable the entire script in Tampermonkey.
By the end of this section, you'll have a foundation for settings management that you can expand for any future script features.
We'll need to expand our HTML to include a checkbox control, and then set up event listeners to respond to user interactions with our script.
// Build HTML content for our tab
const section = document.createElement('div');
section.innerHTML = `
<div>
<h2>Our First Script!</h2>
<section>
<input type="checkbox" id="wazedevEnabled" class="wazedevSettingsCheckbox">
<label for="wazedevEnabled">Enable WazeDev Script</label>
</section>
<hr>
<section>
<h3>User Info</h3>
Username: <a href="#" id="wazedevUsernameLink" target="_blank"><span id="wazedevUsername"></span></a><br>
Rank: <span id="wazedevRank"></span><br>
Area manager: <span id="wazedevAM"></span><br>
Country manager: <span id="wazedevCM"></span>
</section>
<hr>
<section>
<h3>Detailed Profile</h3>
<div id="wazedevProfileLoading">Loading profile data...</div>
<div id="wazedevProfileData" style="display: none;">
Total Edits: <span id="wazedevTotalEdits"></span><br>
Today's Edits: <span id="wazedevTodayEdits"></span>
</div>
</section>
<hr>
<section id="wazedevEditsByType" style="display: none;">
<h3>Edits by Type</h3>
Segments: <span id="wazedevSegments"></span><br>
Venues: <span id="wazedevVenues"></span><br>
Map Problems: <span id="wazedevMapProblems"></span><br>
Update Requests: <span id="wazedevUpdateRequests"></span><br>
Place Update Requests: <span id="wazedevPlaceUpdateRequests"></span><br>
Segment House Numbers: <span id="wazedevSegmentHouseNumbers"></span>
</section>
</div>
`;
Now we need to add our state management system. First, let's declare a settings object and helpers in your script's top-level scope:
(function() {
'use strict';
const SCRIPT_NAME = GM_info.script.name; // Get the Script name from the @name tag of the UserScript Meta Keys
const STORAGE_KEY = 'wazedev_Settings'; // Local Storage key
let wmeSDK; // Declare wmeSDK globally
let settings = {}; // Global settings object
// Initialization of the WME SDK
With adding this option we are going to want to add a save and load method so we can save this to the browser's localStorage
and retrieve it into an object that we can use to check against in the script.
Lets start with the save method:
// Save settings to localStorage
function saveSettings() {
try {
const localSettings = { Enabled: settings.Enabled };
localStorage.setItem(STORAGE_KEY, JSON.stringify(localSettings));
console.log(`${SCRIPT_NAME}: Settings saved`, localSettings);
} catch (error) {
console.error(`${SCRIPT_NAME}: Error saving settings:`, error);
}
}
We create a saveSettings method and which defines a localsettings
object which pulls the settings from a settings
object defined globally in the script. Once the localsettings
object's settings have been set, set the localStorage
setting and change the localsettings
object to JSON as a parameter to the setItem
method. Your settings are now stored in localStorage in the browser. Now we need to handle the user enabling/disabling the Enabled checkebox and save it to our settings.
Now that we have saving set up, lets get loading working. For this we will create a loadSettings
method which will retrieve our script's settings from localStorage
. To do this we will use JavaScript's built-in JSON.parse() method to parse our localStorage
settings into an object that we can interact with. We will then set up an object with default settings, in case a storage object doesn't exist or we have added options to the script that do not exist locally. We will start with the script disabled, so the user doesn't experience strange behavior after installing/updating (usually a good practice, depending on what your script is doing). Once that is set up, the function will loop the settings object property and add any missing settings to our settings
object that we will define globally in our script.
// Load settings from localStorage
function loadSettings() {
try {
const loadedSettings = JSON.parse(localStorage.getItem(STORAGE_KEY));
const defaultSettings = { Enabled: false };
settings = loadedSettings ? loadedSettings : defaultSettings;
// Add missing default settings
for (const prop in defaultSettings) {
if (!settings.hasOwnProperty(prop)) {
settings[prop] = defaultSettings[prop];
}
}
console.log(`${SCRIPT_NAME}: Settings loaded`, settings);
} catch (error) {
console.error(`${SCRIPT_NAME}: Error loading settings:`, error);
settings = { Enabled: false };
}
}
This method should be called once our script's tab is added to the side bar, before set add any event handlers. To do this, we will call loadSettings
at the start of the main
method and then set the Enabled checkbox based on the loaded settings. In order to set the checkbox state, we will create a helper method called setChecked
which will make it easy to check/uncheck any checkboxes we use on the settings tab.
In order to detect when the user changes the Enabled setting, we will add a handler for the change event for the checkbox. This will automagically pull the settings name from the id that we set and set the settings object to the current checkbox status and then save to localStorage. This will allow you to add multiple checkboxes with properly named id's and using the "wazedevSettingsCheckbox" class and have the setting be automatically detected and saved, without having to write code to do it for every checkbox.
// Setup event handlers for our controls
function setupEventHandlers() {
const checkboxes = document.querySelectorAll('.wazedevSettingsCheckbox');
checkboxes.forEach((checkbox) => {
checkbox.addEventListener('change', function () {
// Extract the setting name from the checkbox ID by removing the 'wazedev' prefix
// Example: 'wazedevEnabled' -> 'Enabled'
const settingName = this.id.substring(7);
settings[settingName] = this.checked;
saveSettings();
});
});
}
// Helper to set checkbox state
function setChecked(checkboxId, checked) {
const checkbox = document.getElementById(checkboxId);
if (checkbox) checkbox.checked = checked;
}
Now we need to update our main()
function to load settings, set the checkbox state, and add event handlers:
async function main() {
// Load saved settings first
loadSettings();
// Set the default state of the "Enable" checkbox at script startup
setChecked('wazedevEnabled', settings.Enabled);
// Continue with user info/profile update as before...
// Setup event handlers for checkboxes
setupEventHandlers();
console.log(`${SCRIPT_NAME}: Settings initialized!`);
}
localStorage Persistence:
- Browser-based storage that persists between sessions
- Stores data as strings, so we use JSON.stringify/parse for objects
- Try/catch blocks protect against storage quota or browser restrictions
Event-Driven Architecture:
- Checkbox changes trigger immediate saves
- Generic event handler works for multiple checkboxes using CSS classes
- ID naming convention allows automatic setting name extraction
Default Settings Pattern:
- Always provide fallback defaults for new installations
- Merge defaults with loaded settings to handle script updates
- Graceful degradation when localStorage fails
Scalable Settings System:
- Adding new checkboxes only requires proper ID/class naming
- Settings object can grow without modifying event handlers
- Centralized save/load functions handle all settings uniformly
After implementing these changes, verify:
- Checkbox state persists after refreshing WME
- Console shows save/load messages when toggling the checkbox
- localStorage contains your data (check DevTools → Application → Local Storage)
Troubleshooting:
- Settings don't persist: Check browser's localStorage quota and permissions
- Checkbox doesn't respond: Verify the CSS class and ID naming match exactly
- Console errors on save: localStorage might be disabled in private browsing mode
Below is a full working example combining everything so far.
// ==UserScript==
// @name WazeDev First Script // Display name in Tampermonkey dashboard
// @namespace http://tampermonkey.net/ // Unique identifier (use your GreasyFork profile for updates)
// @version 0.1 // Version number for update tracking
// @description Learning to script! // Brief description of functionality
// @author You // Your name or username
// @include https://beta.waze.com/* // Run on beta WME (testing environment)
// @include https://www.waze.com/editor* // Run on production WME
// @include https://www.waze.com/*/editor* // Run on localized WME URLs
// @exclude https://www.waze.com/user/editor* // Don't run on user profile pages
// @exclude https://www.waze.com/editor/sdk/* // Don't run on SDK documentation
// @grant none // No special permissions needed
// ==/UserScript==
(function () {
'use strict';
const SCRIPT_NAME = GM_info.script.name; // Get the Script name from the @name tag of the UserScript Meta Keys
const STORAGE_KEY = 'wazedev_Settings'; // Local Storage key
let wmeSDK; // Declare wmeSDK globally
let settings = {}; // Global settings object
if (window.SDK_INITIALIZED) {
console.log(`${SCRIPT_NAME}: SDK initialized...`);
window.SDK_INITIALIZED.then(bootstrap).catch((err) => {
console.error(`${SCRIPT_NAME}: SDK initialization failed`, err);
});
} else {
console.warn(`${SCRIPT_NAME}: SDK_INITIALIZED is undefined`);
}
function bootstrap() {
try {
wmeSDK = getWmeSdk({
scriptId: SCRIPT_NAME.replaceAll(' ', ''),
scriptName: SCRIPT_NAME,
});
Promise.all([wmeReady()])
.then(() => {
console.log(`${SCRIPT_NAME}: All dependencies are ready.`);
init();
})
.catch((error) => {
console.error(`${SCRIPT_NAME}: Error during bootstrap`, error);
});
} catch (error) {
console.error(`${SCRIPT_NAME}: Failed to initialize SDK`, error);
}
}
function wmeReady() {
return new Promise((resolve) => {
if (wmeSDK.State.isReady()) {
console.log(`${SCRIPT_NAME}: WME is ready.`);
resolve();
} else {
console.log(`${SCRIPT_NAME}: Waiting for WME to be ready...`);
wmeSDK.Events.once({ eventName: 'wme-ready' })
.then(() => {
console.log(`${SCRIPT_NAME}: WME is ready now.`);
resolve();
})
.catch((error) => {
console.error(`${SCRIPT_NAME}: Error while waiting for WME to be ready:`, error);
});
}
});
}
function init() {
console.log(`${SCRIPT_NAME}: Script initialized successfully!`);
// Build HTML content for our tab
const section = document.createElement('div');
section.innerHTML = `
<div>
<h2>Our First Script!</h2>
<section>
<input type="checkbox" id="wazedevEnabled" class="wazedevSettingsCheckbox">
<label for="wazedevEnabled">Enable WazeDev Script</label>
</section>
<hr>
<section>
<h3>User Info</h3>
Username: <a href="#" id="wazedevUsernameLink" target="_blank"><span id="wazedevUsername"></span></a><br>
Rank: <span id="wazedevRank"></span><br>
Area manager: <span id="wazedevAM"></span><br>
Country manager: <span id="wazedevCM"></span>
</section>
<hr>
<section>
<h3>Detailed Profile</h3>
<div id="wazedevProfileLoading">Loading profile data...</div>
<div id="wazedevProfileData" style="display: none;">
Total Edits: <span id="wazedevTotalEdits"></span><br>
Today's Edits: <span id="wazedevTodayEdits"></span>
</div>
</section>
<hr>
<section id="wazedevEditsByType" style="display: none;">
<h3>Edits by Type</h3>
Segments: <span id="wazedevSegments"></span><br>
Venues: <span id="wazedevVenues"></span><br>
Map Problems: <span id="wazedevMapProblems"></span><br>
Update Requests: <span id="wazedevUpdateRequests"></span><br>
Place Update Requests: <span id="wazedevPlaceUpdateRequests"></span><br>
Segment House Numbers: <span id="wazedevSegmentHouseNumbers"></span>
</section>
</div>
`;
// Register the script tab using the SDK
wmeSDK.Sidebar.registerScriptTab()
.then(({ tabLabel, tabPane }) => {
tabLabel.textContent = 'WazeDev';
tabLabel.title = SCRIPT_NAME;
tabPane.appendChild(section);
// Initialize the main() function after tab is created
main();
})
.catch((error) => {
console.error(`${SCRIPT_NAME}: Error creating script tab`, error);
});
}
// Save settings to localStorage
function saveSettings() {
try {
const localSettings = { Enabled: settings.Enabled };
localStorage.setItem(STORAGE_KEY, JSON.stringify(localSettings));
console.log(`${SCRIPT_NAME}: Settings saved`, localSettings);
} catch (error) {
console.error(`${SCRIPT_NAME}: Error saving settings:`, error);
}
}
// Load settings from localStorage
function loadSettings() {
try {
const loadedSettings = JSON.parse(localStorage.getItem(STORAGE_KEY));
const defaultSettings = { Enabled: false };
settings = loadedSettings ? loadedSettings : defaultSettings;
// Add missing default settings
for (const prop in defaultSettings) {
if (!settings.hasOwnProperty(prop)) {
settings[prop] = defaultSettings[prop];
}
}
console.log(`${SCRIPT_NAME}: Settings loaded`, settings);
} catch (error) {
console.error(`${SCRIPT_NAME}: Error loading settings:`, error);
settings = { Enabled: false };
}
}
// Setup event handlers for our controls
function setupEventHandlers() {
const checkboxes = document.querySelectorAll('.wazedevSettingsCheckbox');
checkboxes.forEach((checkbox) => {
checkbox.addEventListener('change', function () {
// Extract the setting name from the checkbox ID by removing the 'wazedev' prefix
// Example: 'wazedevEnabled' -> 'Enabled'
const settingName = this.id.substring(7);
settings[settingName] = this.checked;
saveSettings();
});
});
}
// Helper to set checkbox state
function setChecked(checkboxId, checked) {
const checkbox = document.getElementById(checkboxId);
if (checkbox) checkbox.checked = checked;
}
async function main() {
// Load saved settings first
loadSettings();
// Set the default state of the "Enable" checkbox at script startup
setChecked('wazedevEnabled', settings.Enabled);
// Continue with user info/profile update
const userSession = wmeSDK.State.getUserInfo();
// Set basic user info
document.getElementById('wazedevUsername').textContent = userSession.userName;
document.getElementById('wazedevRank').textContent = userSession.rank;
document.getElementById('wazedevAM').textContent = userSession.isAreaManager ? 'Yes' : 'No';
document.getElementById('wazedevCM').textContent = userSession.isCountryManager ? 'Yes' : 'No';
// Set profile link
const profileLink = wmeSDK.DataModel.Users.getUserProfileLink({ userName: userSession.userName });
document.getElementById('wazedevUsernameLink').href = profileLink;
// Fetch detailed user profile
try {
const userProfile = await wmeSDK.DataModel.Users.getUserProfile({ userName: userSession.userName });
document.getElementById('wazedevProfileLoading').style.display = 'none';
document.getElementById('wazedevProfileData').style.display = 'block';
document.getElementById('wazedevEditsByType').style.display = 'block';
document.getElementById('wazedevTotalEdits').textContent = userProfile.totalEditCount.toLocaleString();
const todayEdits = userProfile.dailyEditCount[userProfile.dailyEditCount.length - 1] || 0;
document.getElementById('wazedevTodayEdits').textContent = todayEdits;
const editTypes = userProfile.editCountByType;
document.getElementById('wazedevSegments').textContent = editTypes.segments.toLocaleString();
document.getElementById('wazedevVenues').textContent = editTypes.venues.toLocaleString();
document.getElementById('wazedevMapProblems').textContent = editTypes.mapProblems.toLocaleString();
document.getElementById('wazedevUpdateRequests').textContent = editTypes.updateRequests.toLocaleString();
document.getElementById('wazedevPlaceUpdateRequests').textContent = editTypes.placeUpdateRequests.toLocaleString();
document.getElementById('wazedevSegmentHouseNumbers').textContent = editTypes.segmentHouseNumbers.toLocaleString();
console.log(`${SCRIPT_NAME}: Profile data loaded successfully`);
} catch (error) {
console.error(`${SCRIPT_NAME}: Error fetching user profile:`, error);
document.getElementById('wazedevProfileLoading').textContent = 'Error loading profile data';
}
// Setup event handlers for checkboxes
setupEventHandlers();
console.log(`${SCRIPT_NAME}: Settings initialized!`);
}
})();
Our script is now saving & loading the Enabled setting! Hooray!
In the next section we will add drawing of the lines when segments & Places are selected.