Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
Skins
  • Light
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Default (Cyborg)
  • No Skin
Collapse
Diagram Community Logo
W

Wojtek Krzesaj

@Wojtek Krzesaj
About
Posts
4
Topics
3
Groups
0
Followers
0
Following
0

Posts

Recent Best Controversial

  • Panel Auto truncating content
    W Wojtek Krzesaj

    🐛 Problem

    One of the most commonly used panel types in GoJS is the Auto panel. It adjusts its size according to the content.
    Adding go.Shape as first element to an Auto panel will result in the shape being used as a background for other elements added to the panel after it.
    Depending on the shape’s figure we use for the background, by setting the figure property, it changed the properties responsible for panel Auto positioning. These properties are spot1 and spot2, whose default values are the top left corner for spot1 and bottom right corner for spot2.
    An example of shape that would change these properties is RoundedRectangle, where the spot1 and spot2 properties will be compensated by the value parameter1 * 0.3 where parameter1 is equal to the border radius of the RoundedRectangle shape. This behaviour prevents content from extending beyond the shape's outline.
    Unfortunately, this leads to the situation of cutting of the content of the Auto panel, which tries to use all its space (responsiveness).

    💭 Example

    Let’s look at the example of such behaviour.
    A panel of type Auto with the light blue, rounded at the top, rectangle background, and inside a panel of type Table, with stretch set to go.Stretch.Horizontal, positioning the text. The Auto panel is placed inside a GoJS Node with desiredSize property defined.
    p0a.png
    At the picture above we can see a red parent Auto panel, blue background shape and a TextBlock with yellow background inside a Table panel.
    Since we set stretch to go.Stretch.Horizontal in order to stretch the Table panel to always use the entire available width of the parent panel we encounter a situation where our text is cut off.
    p1a.png
    And without extra background colours:
    p1b.png
    This is because the Table panel will have the same width as the parent panel (Auto), which in turn has compensated spot1 and spot2 properties by which the extended content of the Table panel will be truncated.

    🎯 Solution

    In order to fix this problem, we need to override the values of the properties responsible for content positioning in the Auto panel. In the properties of the panels background shape (go.Shape) we set values for spot1 and spot2 as follows:

    spot1: go.Spot.TopLeft,
    spot2: go.Spot.BottomRight,
    

    The result will be as follows:
    p2a.png
    p2b.png
    As we can see, the text is no longer cut off, however it sticks out beyond the rounded corners of the panels Auto background shape. Because of the setting spot1 and spot2 properties back to uncompensated values, we have to take care of the correct positioning of the panel content ourselves so that it does not stick out beyond the parent panel. To do this, we can use margins. We add a margin with the value new go.Margin(0, 10) to the Table panel to achieve the following result.
    p3a.png
    p3b.png
    This way we fixed the truncated text, and our Table panel is still responsive without the need to calculate its width manually 🎉


  • Leveraging AbortController for GoJS Integration
    W Wojtek Krzesaj

    Have you ever forgotten to remove an event listener, or accidentally removed the wrong one? Maybe you’ve used an arrow function when registering a listener, only to realize later that you need the exact same reference to unregister it. These common pitfalls can lead to memory leaks and unexpected behaviour in your applications.

    Thankfully, AbortController provides a clean and efficient way to manage event listeners and cancel async operations. While it’s often associated with aborting fetch requests [1], it’s also a powerful tool for handling event listeners in a more controlled and maintainable way. Let’s explore how this concept can help us write better, cleaner code.

    ⚾ Web requests and Fetch

    Since AbortController is often introduced as a way to cancel web requests, let’s start with how it works alongside fetch.
    Typically, when making a request, you might write:

    const response = await fetch(url);
    

    However, this approach doesn’t provide a way to cancel the request if it’s no longer needed. For example, if a user navigates away or clicks a "Cancel" button, the request will still complete, potentially wasting resources.

    But here is where AbortController comes to the rescue. We can modify our code to be cancellable.

    const controller = new AbortController();
    const signal = controller.signal;
    const response = await fetch(url, { signal });
    

    Now, if we decide to cancel the request at any point, we can simply call:

    controller.abort();
    

    When abort() is called, the fetch request is immediately terminated, and the promise rejects with an AbortError. This is particularly useful in scenarios where a user cancels an action, switches pages, or when we want to prevent unnecessary network requests.

    ⚛️ Event listeners in React

    Another powerful use case for AbortController is managing event listeners more efficiently. You might find yourself often writing React code like this:

    useEffect(() => {
      window.addEventListener('resize', handleResize)
      window.addEventListener('hashchange', handleHashChange)
      window.addEventListener('storage', handleStorageChange)
    
      return () => {
        window.removeEventListener('resize', handleResize)
        window.removeEventListener('hashchange', handleHashChange)
        window.removeEventListener('storage', handleStorageChange)
      }
    }, [])
    

    While this approach works, manually removing each event listener can become tedious and error-prone, especially as the number of event listeners grows.
    Instead, we can simplify our cleanup logic by leveraging AbortController. Since addEventListener supports passing an AbortSignal as an option, we can register multiple event listeners and remove them all at once in the cleanup function:

    useEffect(() => {
      const controller = new AbortController();
      const { signal } = controller;
    
      window.addEventListener('resize', handleResize, { signal });
      window.addEventListener('hashchange', handleHashChange, { signal });
      window.addEventListener('storage', handleStorageChange, { signal });
    
      return () => controller.abort(); // Removes all listeners at once
    }, []);
    

    This small change makes event handling in React applications more manageable, especially when dealing with multiple listeners in components that frequently mount and unmount [2].

    🎉 AbortController with GoJS

    Managing window event listeners is one use case, but the same concept can be applied to the GoJS library. According to its documentation, we can register listeners for multiple different events in a similar way to how we handle window events [3].

    const listener = (e: go.DiagramEvent) => {
      const part = e.subject.part;
      if (!(part instanceof go.Link)) {
        console.log('Clicked on ' + part.data.text);
      }
    };
    diagram.addDiagramListener('ObjectSingleClicked', listener);
    

    In this example, clicking on a node logs a message to the console. To remove the event listener, we simply call:

    diagram.removeDiagramListener('ObjectSingleClicked', listener);
    

    However, when dealing with multiple diagram events and model change listeners, managing their removal can quickly become complex and difficult to track. Unfortunately, we can’t simply pass an AbortController signal as a third parameter like we do with window.addEventListener. However, we can adjust how we register our listeners to leverage the same technique. The key is to listen for the 'abort' event whenever we register a listener, allowing us to clean them up efficiently. Here's how it can be done:

    function makeAddDiagramEventListener(diagram: go.Diagram) {
      return (
        eventName: go.DiagramEventName,
        callback: go.DiagramEventHandler,
        options: { signal?: AbortSignal } = {}
      ) => {
        options.signal?.addEventListener("abort", () => {
          diagram.removeDiagramListener(eventName, callback);
        });
        return diagram.addDiagramListener(eventName, callback);
      };
    }
    

    Here, we define a higher-order function that first takes the diagram we’re working with. Inside, we register the event listener while also checking if an AbortSignal is provided. If a signal is available, we attach a listener to its abort event to ensure the diagram event listener is properly removed when aborted. Using this function, we can register a 'ObjectSingleClicked' event on our diagram like this:

    const controller = new AbortController();
    makeAddDiagramEventListener(diagram)('ObjectSingleClicked', listener, { signal: controller.signal });
    

    This way we can add multiple event listeners and be sure we remove them all by:

    controller.abort();
    

    The same logic can be applied when working with GoJS model change listeners.
    The higher-order method for mode changed listener could look like this:

    function makeAddModelChangedListener(diagram: go.Diagram) {
      return (
        callback: go.ChangedEventHandler,
        options: { signal?: AbortSignal } = {}
      ) => {
        options.signal?.addEventListener("abort", () => {
          diagram.removeModelChangedListener(callback);
        });
        return diagram.addModelChangedListener(callback);
      };
    }
    

    It's worth noticing that even if we add both diagram event listeners and model changed listeners we still remove them all by calling abort method on the Abort Controller.

    🎯 Conclusion

    By integrating AbortController, we can make our code more maintainable, scalable, and efficient, especially when dealing with multiple event listeners. Instead of manually tracking and removing listeners, we can leverage a single abort() call to clean everything up effortlessly.

    If you're working with GoJS, consider adopting AbortController to keep your codebase clean and prevent potential memory leaks. Small optimizations like this can significantly improve the maintainability and reliability of your applications.

    Happy coding! 🚀

    📃 Source

    1. https://developer.mozilla.org/en-/docs/Web/API/AbortController
    2. https://kettanaito.com/blog/dont-sleep-on-abort-controller
    3. https://gojs.net/latest/intro/events.html
    4. https://youtu.be/2sdXSczmvNc?feature=shared
    5. https://betterstack.com/community/guides/scaling-/understanding-abortcontroller/
  • Login

  • Don't have an account? Register

Powered by NodeBB Contributors
  • First post
    Last post
0
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups