5

I want to merge the properties/values of an object with a class instance. (I'm not sure what the correct terminology is in JS, but the example should clarify)

My attempts were with the spread syntax. See below.

I have a File-instance:

const files = listOfFilesFromDragNdrop();
let file = files[0];

console.log(file)

Outputs something like:

File(2398)
lastModified: 1530519711960
lastModifiedDate: Mon Jul 02 2018 10:21:51 GMT+0200
name: "my_file.txt"
preview: "blob:http://localhost:8080/6157f5d5-925a-4e5d-a466-24576ba1bf7c"
size: 2398
type: "text/plain"
webkitRelativePath: ""

After this is added, I use FileReader.readAsText() to obtain the contents, and wrap it in an object like:

contentObject = getFileContentFromFile()
console.log(contentObject)

Will output something like:

{ 
    preview: "blob:http://localhost:8080/6157f5d5-925a-4e5d-a466-24576ba1bf7c",
    content: "Lorem ipsum some text here." 
}

I would like to end up with a merged object like:

{ 
    // "preview" is the key used to map files and content
    preview: "blob:http://localhost:8080/6157f5d5-925a-4e5d-a466-24576ba1bf7c",

    // "text" is the new field with the content from contentObject
    text: "Lorem ipsum some text here." 

    // The other fields are from the File instance
    name: "my_file.txt",
    size: 2398,
    type: "text/plain",
    lastModified: 1530519711960,
    // ...        
}

What I first tried was:

const mergedObject = {
    ...file,
    text: contentObject.content
}

and similarily (aware that text key would become content) I tried

const mergedObject = {
    ...file,
    ...contentObject
}

But, Then I only get the contentObject fields, i.e. the mergedObject is similar to contentObject. Interestingly, if I do

const mergedObject = {
    ...file
}

the mergedObject is a File instance. I assume that the spread operator does not work for class instances in the same way as it does for objects? How can I achieve a merged object?

More info that is non-essential

  • The FileReader is implemented in a redux middleware and dispatches a new action with the { preview: '1234..ef', text: 'Lorem ipsum'} object as payload after it has completed the read.
  • I'm mapping the content to the file with the preview-field, and want to return the merged object in a "files"-reducer with something like: return files.map(file => file.preview !== payload.preview? file: {...file, text: payload.content}
3
  • You say you're using FileReader.readAsText "to obtain the contents, e.g. in an object". Isn't that going to return a string, not an object? Maybe I'm misunderstanding a step. Commented Jul 10, 2018 at 16:56
  • You are correct, but it's async so I wrap it in an object to handle the callback. Commented Jul 10, 2018 at 16:57
  • My example is a bit simplified, because it's part of a Redux reducer and Middleware Commented Jul 10, 2018 at 17:00

4 Answers 4

6

To merge a class instance and an object, in ES6, you can use Object.assign() to merge all the properties in object and maintain the original prototype for class instance. Spread operator only merge all the properties but not prototypes.

In you case, try:

const mergedObject = Object.assign(file, contentObject)

Remember in this case your original file object will be changed.

Sign up to request clarification or add additional context in comments.

Comments

1

You may just have to do something like this...

const mergedObject = {
  lastModified: file.lastModified,
  lastModifiedDate: file.lastModifiedDate,
  name: file.name,
  size: file.size,
  type: file.type,
  webkitRelativePath: file.webkitRelativePath,
  text: contentObject.content,
  preview: contentObject.preview,
}

You could write a utility function to pull the pseudo properties from the file instance:

// Error is a like File with a pseudo property named message
let error = new Error('my error message')
error.status = 404;

const pick = (objectLike, properties) =>
    properties.reduce(
        (acc, key) => {
            acc[key] = objectLike[key];
            return acc;
        },
        {}
    );

const contentObject = {
    content: 'content text',
    preview: 'http://url.io',
};

const mergedObject = {
  ...pick(error, Object.getOwnPropertyNames(error)),
  ...contentObject,
}
console.log(JSON.stringify(mergedObject));

Lodash has a pick function you could use for this.

4 Comments

I hoped that I didn't have to do that, but it seems like that is the only apporach. Thanks.
@ThomasFauskanger, I updated the answer with a possible utility method you could write to get closer to what I think you want.
Thanks for the added, proposed solution. (What I actually did to get around this in my solution was to embed the File instance together with the text in a wrapped object like const allInformation = {instance: file, text: content)}.)
Note that this technique won't create references for properties defined in parent classes that the object class might be extending.
1

Spread syntax like loops iterates over enumerable properties. And as you can see the code below shows that name property of a File object is not enumerable. So the only way to get those properties is one by one.

document.querySelector('input').addEventListener('change', e => {
  const file = e.target.files[0];
  console.log(file.propertyIsEnumerable('name'));
});
<input type="file">

1 Comment

Thanks for the explanation for why spread syntax does not work with the File instance.
0

You can walk up the prototype chain and create a POJO that wraps around the class instance. Once you have that the merge is trivial.

// just a sample object hierarchy

class Plant {
  constructor() {
    this._thorns = false;
  }
  hasThorns() {
    return this._thorns;
  }
}

class Fruit extends Plant {
  constructor(flavour) {
    super();
    this._flavour = flavour;
  }
  flavour() {
    return this._flavour;
  }
}

// this is the mechanism

function findProtoNames(i) {
  let names = [];
  let c = i.constructor;
  do {
    const n = Object.getOwnPropertyNames(c.prototype);
    names = names.concat(n.filter(s => s !== "constructor"));
    c = c.__proto__;
  } while (c.prototype);

  return names;
}

function wrapProto(i) {
  const names = findProtoNames(i);
  const o = {};
  for (const name of names) {
    o[name] = function() {
      return i[name].apply(i, arguments);
    }
  }

  return o;
}

function assignProperties(a, b) {
  
  for (const propName of Object.keys(b)) {
    if (a.hasOwnProperty(propName)) {
      const msg = `Error merging ${a} and ${b}. Both have a property named ${propName}.`;
      throw new Error(msg);
    }

    Object.defineProperty(a, propName, {
      get: function() {
        return b[propName];
      },
      set: function(value) {
        b[propName] = value;
      }
    });
  }

  return a;
}

function merge(a, b) {
  if (b.constructor.name === "Object") {
    return Object.assign(a, b);
  } else {
    const wrapper = wrapProto(b);
    a = assignProperties(a, b);
    return assignProperties(a, wrapper);
  }
}

// testing it out

const obj = {
  a: 1,
  b: 2
};
const f = new Fruit('chicken');
const r = merge(obj, f);
console.log(r);
console.log('thorns:', r.hasThorns());
console.log('flavour:', r.flavour());

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.