44

JSON.stringify(eventObject);

gives:

TypeError: Converting circular structure to JSON


dojox.json.ref.toJson(eventObject);

gives:

TypeError: Accessing selectionEnd on an input element that cannot have a selection.


Is there some library/code ready to use to accomplish it ?

Tar
  • 8,529
  • 9
  • 56
  • 127
  • 1
    Are you trying to view the structure to extract certain method(s) from the object or what is your purpose for wanting to `stringify` it? – Robert Jul 18 '12 at 18:11
  • debugging PhoneGap - send data structures, events and stack traces to native layer – Tar Jul 20 '12 at 10:18

7 Answers7

22

You won't be able to serialize an event object with JSON.stringify, because an event object contains references to DOM nodes, and the DOM has circular references all over the place (e.g. child/parent relationships). JSON can't handle these by default, so you're a bit out of luck there.

I'd suggest to look at How to serialize DOM node to JSON even if there are circular references? which has a few suggestions on how to serialize a DOM node. Also, the following questions seem to have useful information:

JSON libraries able to handle circular references seem to be

Alternatively, you could delete all references to DOM nodes if you don't need them, and then serialize the object. You shouldn't do this after all. See @PointedEars comment :)

Community
  • 1
  • 1
fresskoma
  • 25,481
  • 10
  • 85
  • 128
  • 2
    Event objects are [host objects](http://ecma-international.org/ecma-262/5.1/#sec-4.3.8). [Do not mess with them](http://ecma-international.org/ecma-262/5.1/#sec-8.6.2), such as trying to delete their properties or assign to properties that are not supposed to be assigned to. – PointedEars Jul 18 '12 at 18:28
19

Use the "replacer" function to avoid errors:

JSON.stringify(evt, function(k, v) {
    if (v instanceof Node) {
        return 'Node';
    }
    if (v instanceof Window) {
        return 'Window';
    }
    return v;
}, ' ');

Update 2019: the browser API has changed some way, here is a method to expose all available keys in Event prototype chain

function stringifyEvent(e) {
  const obj = {};
  for (let k in e) {
    obj[k] = e[k];
  }
  return JSON.stringify(obj, (k, v) => {
    if (v instanceof Node) return 'Node';
    if (v instanceof Window) return 'Window';
    return v;
  }, ' ');
}
Alexander Shutau
  • 2,660
  • 22
  • 32
7

I had a similar problem and wrote a simple event serializer with a helper method to cleanup the event's path attribute. The approach for this solution to transform data from the event to a serializable object:

  • Copy over primitive attributes
  • Copy outerHTML for element attributes in the event object
  • Calculate selector path for the path attribute (this avoids copying the outerHTML of the entire HTML page)

// Calculate a string representation of a node's DOM path.
var pathToSelector = function(node) {
  if (!node || !node.outerHTML) {
    return null;
  }

  var path;
  while (node.parentElement) {
    var name = node.localName;
    if (!name) break;
    name = name.toLowerCase();
    var parent = node.parentElement;

    var domSiblings = [];

    if (parent.children && parent.children.length > 0) {
      for (var i = 0; i < parent.children.length; i++) {
        var sibling = parent.children[i];
        if (sibling.localName && sibling.localName.toLowerCase) {
          if (sibling.localName.toLowerCase() === name) {
            domSiblings.push(sibling);
          }
        }
      }
    }

    if (domSiblings.length > 1) {
      name += ':eq(' + domSiblings.indexOf(node) + ')';
    }
    path = name + (path ? '>' + path : '');
    node = parent;
  }

  return path;
};

// Generate a JSON version of the event.
var serializeEvent = function(e) {
  if (e) {
    var o = {
      eventName: e.toString(),
      altKey: e.altKey,
      bubbles: e.bubbles,
      button: e.button,
      buttons: e.buttons,
      cancelBubble: e.cancelBubble,
      cancelable: e.cancelable,
      clientX: e.clientX,
      clientY: e.clientY,
      composed: e.composed,
      ctrlKey: e.ctrlKey,
      currentTarget: e.currentTarget ? e.currentTarget.outerHTML : null,
      defaultPrevented: e.defaultPrevented,
      detail: e.detail,
      eventPhase: e.eventPhase,
      fromElement: e.fromElement ? e.fromElement.outerHTML : null,
      isTrusted: e.isTrusted,
      layerX: e.layerX,
      layerY: e.layerY,
      metaKey: e.metaKey,
      movementX: e.movementX,
      movementY: e.movementY,
      offsetX: e.offsetX,
      offsetY: e.offsetY,
      pageX: e.pageX,
      pageY: e.pageY,
      path: pathToSelector(e.path && e.path.length ? e.path[0] : null),
      relatedTarget: e.relatedTarget ? e.relatedTarget.outerHTML : null,
      returnValue: e.returnValue,
      screenX: e.screenX,
      screenY: e.screenY,
      shiftKey: e.shiftKey,
      sourceCapabilities: e.sourceCapabilities ? e.sourceCapabilities.toString() : null,
      target: e.target ? e.target.outerHTML : null,
      timeStamp: e.timeStamp,
      toElement: e.toElement ? e.toElement.outerHTML : null,
      type: e.type,
      view: e.view ? e.view.toString() : null,
      which: e.which,
      x: e.x,
      y: e.y
    };

    console.log(JSON.stringify(o, null, 2));
  }
};

// Create a mock event for this example
var evt = new MouseEvent("click", {
  bubbles: true,
  cancelable: true,
  view: window
});
var cb = document.getElementById("clicker");

// Add a click listener
cb.addEventListener("click", serializeEvent);

// Fire the event
cb.dispatchEvent(evt);
<div>
  <button id="clicker" /> JSONify my click!
</div>
Josh Hibschman
  • 3,148
  • 1
  • 25
  • 27
  • This should be the accepted answer. Yes, you need to customize the inner workings slightly based on your needs, but this is great overall! A few minor additions if you want to recreate the entire event: to find what event needs to be executed, do string operations on `o.eventName`, replace `o.view` with an object (usually `window`) and get your target element by parsing the `o.target`, e.g. see [here](https://stackoverflow.com/a/3104237/13078832) – npetrov937 Dec 01 '22 at 10:50
5

Improved version of the code given by Alexander Shutau, as this one handles multi level objects (ES6):

function stringify_object(object, depth=0, max_depth=2) {
    // change max_depth to see more levels, for a touch event, 2 is good
    if (depth > max_depth)
        return 'Object';

    const obj = {};
    for (let key in object) {
        let value = object[key];
        if (value instanceof Node)
            // specify which properties you want to see from the node
            value = {id: value.id};
        else if (value instanceof Window)
            value = 'Window';
        else if (value instanceof Object)
            value = stringify_object(value, depth+1, max_depth);

        obj[key] = value;
    }

    return depth? obj: JSON.stringify(obj);
}

Just call it like this:

stringify_object(event, 2);

For example, on a touchstart event, I'm getting this:

touchstart : {"isTrusted":true,"touches":{"0":{"identifier":0,"target":{"id":"screen"},"screenX":548,"screenY":281.5,"clientX":498.1817932128906,"clientY":185.90908813476562,"pageX":498.1817932128906,"pageY":185.90908813476562,"radiusX":29.77272605895996,"radiusY":27.954544067382812,"rotationAngle":0,"force":0.5},"length":1,"item":{}},"targetTouches":{"0":{"identifier":0,"target":{"id":"screen"},"screenX":548,"screenY":281.5,"clientX":498.1817932128906,"clientY":185.90908813476562,"pageX":498.1817932128906,"pageY":185.90908813476562,"radiusX":29.77272605895996,"radiusY":27.954544067382812,"rotationAngle":0,"force":0.5},"length":1,"item":{}},"changedTouches":{"0":{"identifier":0,"target":{"id":"screen"},"screenX":548,"screenY":281.5,"clientX":498.1817932128906,"clientY":185.90908813476562,"pageX":498.1817932128906,"pageY":185.90908813476562,"radiusX":29.77272605895996,"radiusY":27.954544067382812,"rotationAngle":0,"force":0.5},"length":1,"item":{}},"altKey":false,"metaKey":false,"ctrlKey":false,"shiftKey":false,"view":"Window","detail":0,"sourceCapabilities":{"firesTouchEvents":true},"which":0,"initUIEvent":{},"NONE":0,"CAPTURING_PHASE":1,"AT_TARGET":2,"BUBBLING_PHASE":3,"type":"touchstart","target":{"id":"screen"},"currentTarget":{"id":"screen"},"eventPhase":2,"bubbles":true,"cancelable":true,"defaultPrevented":false,"composed":true,"timeStamp":192516.7899999651,"srcElement":{"id":"screen"},"returnValue":true,"cancelBubble":false,"path":{"0":{"id":"screen"},"1":{"id":"back"},"2":{"id":""},"3":{"id":""},"4":{},"5":"Window"},"composedPath":{},"stopPropagation":{},"stopImmediatePropagation":{},"preventDefault":{},"initEvent":{}}
Octo Poulos
  • 523
  • 6
  • 5
1

Not sure if it helps, but I just stumbled upon this in the Angular JS documentation:

*Source: https://code.angularjs.org/1.5.5/docs/guide/expression#-event-

/*
 * return a copy of an object with only non-object keys
 * we need this to avoid circular references
 */
function simpleKeys (original) {
  return Object.keys(original).reduce(function (obj, key) {
    obj[key] = typeof original[key] === 'object' ? '{ ... }' : original[key];
    return obj;
  }, {});
}

Now you could do something like:

JSON.stringify(simpleKeys(eventObject));
bjunix
  • 4,458
  • 4
  • 33
  • 33
-1

So, the issue is JSON.stringify seems to bail out as soon as it finds a circular reference. I was anyway not interested in the circularly referenced properties. The way I got the rest of them is

var str = "{"
for (var key in data) {
  if (JSON.stringify(data[key]) !== "") {
    str += key + ":" + data[key]) + ",";
  }
}
str += "}"

This will basically give you the rest of the properties. To avoid JS errors you can put if in try/catch.

Saurabh
  • 69
  • 2
-4

Just use JSON.stringify(event) and event data should be converted to string.

Ali
  • 426
  • 5
  • 8