Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exposing DOM from components. #134

Open
Aferz opened this issue Feb 24, 2020 · 12 comments
Open

Exposing DOM from components. #134

Aferz opened this issue Feb 24, 2020 · 12 comments

Comments

@Aferz
Copy link

Aferz commented Feb 24, 2020

In 2018 I asked in Vue forums if there was an API for replicate the behaviour in React that they named "Ref Forwarding". Vue Forum

I'd like to discuss if this functionality is something that still is not a needed feature.

@LinusBorg replied to that message saying that $refs acts different from React and that's fine. But what about exposing DOM elements? Maybe:

<template>
  <MyComponent ref="comp" />
</template>

<script>
export default {
  mounted () {
    this.$refs.comp.DOM.select.classList.add('custom-class')
  }
}
</script>

or

<template>
  <MyComponent element="comp" />
</template>

<script>
export default {
  mounted () {
    this.$els.comp.select.classList.add('custom-class')
  }
}
</script>

Obviously this API is just an idea. I don't know which would be the perfect API or even if this is an interesting idea.

One benefit of this API would be for 3rd party libraries that want to offer full customization and prefer to expose the DOM element directly instead of multiple props (props for height, width, styles, classes... dunno)

This behaviour can be done right now by just using querySelector API but you would need to know the class names (not a big problem). If you decide to got with a querySelector you could end with a breaking change in the next releases if the author wants/needs to change the DOM structure.

Not big problems anyway. Just an idea.

What do you all think?

@cawa-93
Copy link

cawa-93 commented Feb 24, 2020

Don't you think that it violates the principle of component oriented programming? In fact, any component can interfere with the work of another, update if the latter did not provide a special API?

@leopiccionia
Copy link

I don't know if this is a good idea, for two reasons:

  1. Encapsulation (components lose control over their own public API, making any change a potential breaking change);
  2. Reliability (it just requires that Vue don't reuse a component once to lose all properties not tracked by Vue).

One benefit of this API would be for 3rd party libraries that want to offer full customization and prefer to expose the DOM element directly instead of multiple props (props for height, width, styles, classes... dunno)

A common pattern, that I've seen in both React and Vue, is to use spreadable props. I mean something like this:

<template>
  <div>
    <select v-bind="selectProps">...</select>
  </div>
</template>

<script>
export default {
  props: {
    selectProps: { type: Object, default: () => {} }
  }
}
</script>

@Aferz
Copy link
Author

Aferz commented Feb 29, 2020

Don't you think that it violates the principle of component oriented programming? In fact, any component can interfere with the work of another, update if the latter did not provide a special API?

I don't know if this is a good idea, for two reasons:

  1. Encapsulation (components lose control over their own public API, making any change a potential breaking change);

It could be. But being honest, ref or this.$parent violate this principle too, isn't it?
They are meant as an escape hatch for some scenarios as this new API could be.

A common pattern, that I've seen in both React and Vue, is to use spreadable props. I mean something like this:

Yes, that's a workaround, but passing objects as props is something that even Vue documentation discourages, because of the nature of javascript objects being pointers (maybe this is not true anymore because of Vue 3.0 and new proxy system). Furthermore, it's less readable than splitting the object in many props. I don't remember where I read this, I think in style guide (I can't find it out).

Anyway, thanks for your opinions 👍

@leopiccionia
Copy link

the nature of javascript objects being pointers

On real world, you'd use a computed property or similar to avoid unnecessary re-renders.

@LinusBorg
Copy link
Member

We don't have a concept of a forward ref as React has (hope I don't mix up terminology here), which is basically what you propose here: A way to pass a template ref to a child component, and that child can determine to which of its child elements it wants to apply it. Which means the parent doesn't have to be aware of the dom structure or class names in the child to find the "reference element".

Might be worth discussing - i don't think that something like your proposed $els is the right way to go though.

We could also solve this with a template ref + function in Vue 3, like this:

setup(){
  const el = ref(null)
  const forwardRef = _el => el.value = _el

  return (
    <MyChildComponent forwardRef={forwardRef}/>
  )
}

Child:

<div>
  <input ref="forwardRef"
</div>

This should work I think.

@reed-jones
Copy link

If you are manually adding/removing classes, won't your changes get overridden if a parent component has to re-render since the vDom hasn't been updated and is unaware of the changes? (I believe this was the case in Vue 2, and assume it still is with Vue 3, but I really am not sure about that)

@LinusBorg
Copy link
Member

LinusBorg commented Mar 7, 2020

Adding a class is just a trivial example and not the point.

The point is that sometimes, a parent might need to get a hold of an element that belongs to a child. Those situations might be edge cases, often tied to the need to use a certain third party library that works more imperatively, but these use cases do exist.

@reed-jones
Copy link

sure, but won't any native DOM manipulation outside of vue.js run into the same issue? If you are grabbing an element and reading a value from it or whatever this makes sense to me, but if you are updating/changing wouldn't this be an issue with the virtual dom not tracking changes? I only mentioned classes since the original issue's example was adding a class which seems to me it might cause a problem?

@LinusBorg
Copy link
Member

Sure but again: it's just an example and not the point of the functionality we are discussing.

@Aferz
Copy link
Author

Aferz commented Apr 10, 2020

I'll give you all an example that I've faced recently.

Imagine a custom video component:

<template>
  <video></video>
</template>

The problem I have found in a component like this is really weird to try to control it based on props. I assume this kind of components are tightly coupled and their intended use is via $refs.ref.method().

As you can see, I haven't defined methods for this component. If I want to play() the video from a parent component (Which is probably always), I've to define the methods I want to expose from the <video> tag.

<template>
  <video ref="video"></video>
</template>

<script>
export default {
  methods: {
    play () {
      this.$refs.video.play()
    }
  }
}
</script>

Now from the parent, I can just do this:

<template>
  <MyVideo ref="video" />
</template>

<script>
export default {
  components: {
    MyVideo
  },

  created () {
    this.$refs.video.play()
  }
}
</script>

Nice! This works. But all this component stuff is about composition, right? Currently, in my project we have many different video components that are composed between 1 and 3 layers of components.

This means we had to just copy/paste this piece of code for every component we want to compose over the first MyVideo component ...

<script>
export default {
  methods: {
    play () {
      this.$refs.video.play()
    },

    pause () {
      this.$refs.video.pause()
    }
  }
}
</script>

... to be able to play/pause the video.

This could be solved if a parent could get access to a piece of DOM of a child component, via forwardRef or any other mechanism. As @LinusBorg said:

Child:

<template>
  <video :ref="videoRef" />
</template>

<script>
export default {
  props: ['videoRef']
}
</script>

Parent:

<script>
export default {
  setup () {
    const el = ref(null)
    const videoRef = _el => el.value = _el

    return (
      <MyChildComponent videoRef={videoRef}/>
    )
  }
}
</script>

The only thing with this approach is I don't really know the disadvantages of exposing the DOM like this.

  1. What happens if the forwardRef is applied to another DOM that was created after applying to the first? It will react to changes?
  2. What happens if the forwardRef is applied to a v-for?
  3. Etc...

This is an edge case? Yes
This could be done via props and watchers? Probably

The point of this issue is to raise that this problem exists and this is not even a third-party package. There are some tags in HTML5 that are designed to be used in an imperative way like <audio> or <video>.

@backbone87
Copy link

backbone87 commented Apr 10, 2020

This could be done via props and watchers? Probably

i would say it should be done via props and watchers. a video player has states (video source, playing, volume, playback position) which make up a very intuitive reactive state.

and if you want to use the DOM video API directly in a component there is no need to use a dedicated video component at all. instead you can just do this in the parent element:

<template>
  <div>
    <video :ref="video" ... />
  </div>
</template>
<script>
export default defineComponent((props) => {
  const video = ref();

  // if you need custom API:
  const videoControl = useMyCustomVideoControlHelpers(video);
  // do something with videoControl: expose it to template for inline listeners, use it in watchers, whatever

  return { video };
});
</script>

@Aferz
Copy link
Author

Aferz commented Apr 10, 2020

i would say it should be done via props and watchers. a video player has states (video source, playing, volume, playback position) which make up a very intuitive reactive state.

I don't think this will be a good approach. How, for example, would you implement rewind 10 seconds via props and watchers? Or move the cursor to a specific second? That will be a mess of watchers, internal state and props. Exposing the DOM will be a cleaner and more maintainable approach IMO.

and if you want to use the DOM video API directly in a component there is no need to use a dedicated video component at all. instead you can just do this in the parent element

I think you are assuming too much here. Our video component has more things that I didn't expose here because it was only an example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants