loading...
Cover image for A JavaScript interview question asked at Facebook
Coderbyte

A JavaScript interview question asked at Facebook

elisabethgross profile image elisabethgross Updated on ・3 min read

Wow what a week! Last week’s challenge was a big hit. In case you missed it, here's a link to last week’s article here and the challenge on Coderbyte.

Before I begin talking about the solution to the challenge, I also wanted to let you all know that we at Coderbyte want to hear from you! Have you just had a technical interview and want feedback on how you think you did? Email me at liz.gross@coderbyte.com with interview questions you've been asked and your answers and I'll get back to you with feedback on your solution. Looking forward to hearing from you all!

And now, without further ado, here is a common way to solve this google interview question:

Stack approach:

When I first heard of this question, I immediately thought of using a stack. A stack is a basic data structure where insertion and deletion of elements takes place at the top of the stack. There are normally three basic operations that can be performed on a stack:

  1. inserting an item into a stack (push)
  2. deleting an item from the stack (pop off the top)
  3. displaying the contents of the stack

In javascript, implementing a stack can be as simple as using an array and its push and pop methods. This is an excellent choice of data structure for our problem. As you iterate through the keypresses, you can push them to a stack. As soon as you hit a backspace keypress, just pop the top item off the stack! Here that is in code:

function removeBackspaces(arr) {
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] === '-B' && result.length > 0) {
      result.pop();
    } else if (arr[i] !== '-B') {
      result.push(arr[i]);
    }
  }
  return result;
}

function checkEqualInputs(arr) {
  const [arr1, arr2] = arr.map((e) => e.split(','))
  const result1 = removeBackspaces(arr1);
  const result2 = removeBackspaces(arr2);
  // check if arrays are equal
  return result1.join('') === result2.join('');
}

Big O:

This is a great solution because it is relatively cheap in terms of time and space. The run-time complexity for both time and space is O(n+m) where n is the length of the first string and m is the length of the second. You will only ever have to iterate through each string once, and store stacks with at most the lengths of each string.

This week’s challenge:

This week, we’ll be solving a coding problem that was given in an actual Facebook phone screen interview. Please comment below with your solutions! And be sure to look at the Facebook Interview Questions course on Coderbyte for more Facebook related challenges!

Write a function that takes a DOM element and smoothly animates it from its current position to distance pixels to the right over duration milliseconds. Implement the following function, animate(el, milliseconds, distance)

For example, animate(document.querySelector('#myDiv'), 2000, 100) would move the element with id myDiv 100px to the right over 2 seconds. Implement this function without using jQuery or any other third-party libraries.

element moving

Can’t wait to see what you all come up with. Have fun and happy coding!

Our newsletter 📫

We’re going to be sending out a small, feature reveal snippet every time we release something big, so our community is the first to know when we break out something new. Give us your email here and we'll add you to our "first to know" list :)

Discussion

pic
Editor guide
Collapse
vince_tblt profile image
Vincent Thibault

I would not use WebAnimation API :

  • It reset the element transform so you need to get current transform value at first place to apply your translate and give it back to the API at each call (so you can't reuse the keyframes at all...)
  • It's awesome for sequential animations, that's not the case here.

As for requestAnimationFrame, isn't it overkill too ?


I would use CSS transition for this kind of stuff.

The tricky part is to avoid reseting things at each call, so you need to:

  • Apply the translateX to the transform value (without erasing scale, reseting position, ...).
  • Add the transition rule to the stack without erasing others (opacity ? colors ? ...).
function animate(element, duration, translateX) {
  const MatrixAPI = window.DomMatrix || window.WebKitCSSMatrix; // Just for compatibility issue
  const currentMatrix = getComputedStyle(element).getPropertyValue('transform');
  const newMatrix = new MatrixAPI(currentMatrix).translateSelf(translateX, 0, 0);
  const transitionRules = (element.style.transition || '').split(',').filter(Boolean);

  element.style.transition = transitionRules
     .filter(rule => !/transform/i.test(rule))
     .concat(`transform ${duration}ms`)
     .join(',');

  element.style.transform = newMatrix;
}
Collapse
elisabethgross profile image
Collapse
richardeschloss profile image
Richard Schloss

Maintaining state is not really that big of a deal using the webanimation api. The element's position is always a property of the element. Even though it wasn't asked of the question to maintain the final position, this is what the method could look like if that were a concern:

function animate(elm, milliseconds, distance) {
  const { x } = elm.getBoundingClientRect()
  elm.animate([
    { transform: `translateX(${x}px)` },
    { transform: `translateX(${x + distance}px)` }
  ], {
    duration: milliseconds,
    fill: 'forwards'
  })
}

Still simpler. I would think the question should have also asked for units to be passed in as an argument to make it far more versatile.

I agree with you though! I would prefer to keep all animation logic in CSS. When I first read the question, I was thinking exactly about that, but I was also wondering what Facebook's goal might be in asking this question. Are they hoping the candidate to be aware of the built-in JS methods? If so, it's a good question to see how well the candidate is keeping his/her knowledge current. Or, maybe it's an opportunity for the candidate who is current to teach the interviewer something new. Is Facebook hoping the candidate to be able to do better than the built-in or will they get worried if the candidate goes down a rabbit hole? Etc.

Working example on jsfiddle

Update 12/11/2019: I provide a more complete example below (thanks to Vincent's reasoning)

Thread Thread
vince_tblt profile image
Vincent Thibault

Well actually it's more about building re-usable function you can use in every context (as a NPM package for example) than a real "Facebook Challenge" (who knows what they are really expecting from it ?)

The problem I can see from yours (even if it's a really simple solution) :

  • Causing a reflow because of getBoundingClientRect() (yes, performance can be a problem in mobile development)
  • Reseting pre-defined scale, rotate parameters (transform:rotate() scale(), ...).
  • Has bad position calculation if the element has CSS position relative/absolute/fixed with left parameters assigned.

In fact, this one with same bugs can be rewritten with CSS transition in less lines and better browser support :

// Buggy, please do not use !
function animate(elm, milliseconds, distance) {
  elm.style.transition = `transform ${milliseconds}ms`;
  elm.style.transform = `translateX(${elm.getBoundingClientRect().x + distance}px)`;
}

So yeah, WebAnimation API is powerful and awesome, but should always be used depending of the context (but It was quite fun to see an actual usage, thanks for it !)

Thread Thread
richardeschloss profile image
Richard Schloss

Interesting perspective. I like the points you bring up! Fwiw, I think the API will eventually address those concerns, which I thought would have been addressed by now in 12/2019, but still being worked on (so yes, my mistake, my apologies). Future options I think will preserve state a bit better.

Thread Thread
vince_tblt profile image
Vincent Thibault

Wow interesting I didn’t know about it!
With future options it’s gonna be a real game-changer!

Thread Thread
richardeschloss profile image
Richard Schloss

After giving this a bit more thought...I promise not to beat it death but...you led me on the right track and I created fully working code now (code review welcomed!). The one minor thing missed was that translateX alone won't always move the object to the right. I.e., if the object is rotated 45deg, the translateX will move along the rotated X axis, not to the right of the screen by the requested amount (i.e., 200px diagonally would mean sqrt((200)2 / 2) = 141 px to the right instead of 200px). The extra parameters need to be provided to the transformation matrix. So, they were really also testing us a bit on computational geometry, more than just the animation. With a few tweaks to the code, regardless of API used, the following code should still work regardless of initial transform:

function animate(elm, milliseconds, distance) {
  console.warn(
    'Running an "animate" method that should be named "moveRightOnly"'
  );
  return new Promise((resolve, reject) => {
    // Allow animation to be chained
    if (distance < 0 || milliseconds < 0) {
      reject(
        new Error(
          "Feeling negative? Really? This method only moves things to the right. Sorry!"
        )
      );
    }

    // I think the Matrix API is neat, but the only reason I'm doing it this long-drawn out way is:
    // 1. MDN warns of the API being non-standard, not production-ready
    // 2. ES features make it easy enough to do without it
    let computedStyle = getComputedStyle(elm).getPropertyValue("transform");
    let newStyle
    if (computedStyle === "none") {
      elm.style.transform = "translate(0px, 0px)";
      computedStyle = getComputedStyle(elm).getPropertyValue("transform");
    }

    const params = computedStyle
    .match(/\((.+)\)/)[1]
    .split(/,\s*/)
    .map(s => parseFloat(s));   
    if (computedStyle.includes('matrix3d')) {
      params[12] += distance
      newStyle = `matrix3d(${params.join(', ')})`
    } else {
      params[4] += distance
      newStyle = `matrix(${params.join(', ')})`
    }

    elm.animate(
      [
        {
          transform: computedStyle
        },
        {
          transform: newStyle
        }
      ],
      {
        duration: milliseconds,
        fill: "forwards"
      }
    ).onfinish = resolve;
  });
}

Updated working examples are here: JSFiddle

(Alternative, using the MatrixAPI, is here. The translateSelf method had to be replaced with setMatrixValue)

--> If the "test box" lines up with the "goal box" it passes.

I think this code addresses the key concerns that were brought up. I'd be damn impressed if someone could do this in a 15-minute phone screen! (and I'm pretty sure Facebook would be impressed by the questions you brought up!)

Thread Thread
vince_tblt profile image
Vincent Thibault

Awesome ! You did a really great job !
The only problem I see : they failed on 3D initial parameter (translateZ(1px)) :

  • The webanimation failed because you handle a 3D matrix as a 2D matrix, making it failed.
  • The matrix version failed because it reset the 3D matrix to a 2D matrix.

So you need to make use of matrix.is2D (or check for /matrix3d/ to change the algorithm used but its seems quite painful to do.

Here is a solution of the problem without the need of parsing the matrix data (just generate a 2D translation matrix and multiply it to the current matrix), with this solution you don't need to care about the 2D-3D stuff : jsfiddle.net/vthibault/9Lqyphkm/

Thread Thread
richardeschloss profile image
Richard Schloss

I updated to include 3d, and also to account for when the animation ends, in case it's desired to chain the animation calls. I still prefer to directly set the parameters instead of performing the multiply operation (which can be a lot of multiply-add operations, and a potential source rounding errors if floating points are used).

Collapse
richardeschloss profile image
Richard Schloss

Also, I think the removeBackspaces method can further be reduced with reduce:

function removeBackspaces(arr) {
  return arr.reduce((result, val) => {
    val === '-B' ? result.pop() : result.push(val)
    return result
  }, [])
}

Gives me the same result, I think, and helps guard against typical problems introduced by loops, such as "off-by-one" errors. (array.reduce helps the iteration stay within the bounds of the array size)

Collapse
richardeschloss profile image
Richard Schloss

For the Facebook question, just "cheat"...just create a wrapper around the built-in JS web-animation API and be done with it in 2 seconds (elm.animate(...)). The built-in isn't third party and technically not breaking directions...so...half cheating, I guess. No need to re-invent the wheel...

Collapse
elisabethgross profile image
elisabethgross Author

Generally speaking, most interview questions are given despite well known methods / libraries being around that have already solved a problem! I have often been asked to implement lodash methods for example, even though in the real world, I'd just use lodash.

Collapse
tomshaw profile image
Tom Shaw

With a couple extra parameters.


var el = document.getElementById("myDiv");

function animate(el, totalTime, distance, startTime, position) {
    if (position >= distance) return;

    var expired = (new Date() - startTime) / 1000;
    position = expired * distance / totalTime * 1000; 

    el.style.left = position + 'px';

    requestAnimationFrame(function() {
        animate(el, totalTime, distance, startTime, position);
    });
}

animate(el, 2000, 300, new Date(), 50);

Collapse
elisabethgross profile image
elisabethgross Author

Nice! Although this will move the element from a given position, rather than where it is on the page.

Collapse
tomshaw profile image
Tom Shaw

Thanks! Another fun challenge would be to add a timing function lerp, similar to cubic-bezier(x1, y1, x2, y2) to vary the speed over the course of the duration. It's a leap forward in complexity but quite doable.

Collapse
yairy profile image
Yair

This will only work if the element is absolute positioned.

Collapse
tomshaw profile image
Tom Shaw

Have you tried el.style.transform = "translate(x,y)"

Collapse
bairrada97 profile image
bairrada97

My solution:

const animate = (element, miliseconds, distance) =>{
element.style.transition = all ${miliseconds/1000}s ease;
element.style.transform = translateX(${distance}px) ;

}

animate(document.querySelector('div'), 2000, 100)

Collapse
gixxerblade profile image
Steve Clark 🤷‍♀️

This is what I got.

let animate = (el, milliseconds, distance) => {
  let element = document.getElementById(el);
  element.setAttribute(
    "style",
    `transform:translateX(${distance}px); transition-duration: ${milliseconds}ms;`
  );
};
animate("para", 2000, 500);```

Collapse
iaboelsuod profile image
Ibrahim Zahema

I think the challenge is asking if you know how to use requestanimationframe basically.

Collapse
stephenmirving profile image
Stephen Irving

But that is crazy overkill for this task. The appropriate way to implement this in current year is to use CSS. If an animation can be done without issue in CSS you should never use JS to do it. That is actually kind of a good rule to extrapolate and follow for everything on the front-end: Use JS only for the things you can't do with other technologies.

Collapse
elisabethgross profile image
Thread Thread
richardeschloss profile image
Richard Schloss

And, just a helpful link: Passing Parameters to CSS animation if anyone is interested... this would be the CSS way of doing it. JS events fire (like "scrolling", "mousemove") and the data from those events get passed to the css vars (but maybe someday we'd find it easier to pass an argument to a method in JS instead? easier to test maybe?). I think the webanimations API will surprise us all in [1-2 years?] and challenge us all as to what we do in JS vs CSS. I could be wrong. I'm still thinking about the pros and cons.

Collapse
lgmf profile image
Collapse
elisabethgross profile image