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

Settings Object and Helper Functions

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

Save and Load Methods

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.

Setting Up Checkbox Event Handling

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

Updated main() with State Logic

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

Key Concepts Introduced

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

Testing Your Settings System

After implementing these changes, verify:

  1. Checkbox state persists after refreshing WME
  2. Console shows save/load messages when toggling the checkbox
  3. 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

Putting It All Together

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.

⚠️ **GitHub.com Fallback** ⚠️