В Vue.js может ли компонент обнаруживать изменение содержимого слота

У нас есть компонент в Vue, который представляет собой фрейм, масштабированный по размеру окна, который содержит (в <slot>) элемент (обычно <img> или <canvas>), который масштабируется по размеру фрейма и позволяет панорамировать и масштабировать этот элемент.

Компонент должен реагировать на изменение элемента. Единственный способ, которым мы можем это сделать, — это чтобы родитель подталкивал компонент, когда это происходит, однако было бы намного лучше, если бы компонент мог автоматически определять, когда элемент <slot> изменяется, и реагировать соответствующим образом. Есть ли способ сделать это?


person Euan Smith    schedule 01.07.2016    source источник


Ответы (4)


Насколько мне известно, Vue не предоставляет возможности сделать это. Однако здесь стоит рассмотреть два подхода.

Наблюдение за DOM слота на предмет изменений

Используйте MutationObserver, чтобы определить, когда DOM в <slot> изменяется. Это не требует связи между компонентами. Просто настройте наблюдателя во время обратного вызова mounted вашего компонента.

Вот фрагмент, показывающий этот подход в действии:

Vue.component('container', {
  template: '#container',
  
  data: function() {
  	return { number: 0, observer: null }
  },
  
  mounted: function() {
    // Create the observer (and what to do on changes...)
    this.observer = new MutationObserver(function(mutations) {
      this.number++;
    }.bind(this));
    
    // Setup the observer
    this.observer.observe(
      $(this.$el).find('.content')[0],
      { attributes: true, childList: true, characterData: true, subtree: true }
    );
  },
  
  beforeDestroy: function() {
    // Clean up
    this.observer.disconnect();
  }
});

var app = new Vue({
  el: '#app',

  data: { number: 0 },
  
  mounted: function() {
    //Update the element in the slot every second
    setInterval(function(){ this.number++; }.bind(this), 1000);
  }
});
.content, .container {
  margin: 5px;
  border: 1px solid black;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>

<template id="container">
  <div class="container">
    I am the container, and I have detected {{ number }} updates.
    <div class="content"><slot></slot></div>
  </div>
</template>

<div id="app">
  
  <container>
    I am the content, and I have been updated {{ number }} times.
  </container>
  
</div>

Использование излучения

Если компонент Vue отвечает за изменение слота, то лучше всего генерировать событие когда это изменение произойдет. Это позволяет любому другому компоненту реагировать на сгенерированное событие, если это необходимо.

Для этого используйте пустой экземпляр Vue в качестве глобальной шины событий. Любой компонент может генерировать/прослушивать события на шине событий. В вашем случае родительский компонент может генерировать событие «обновленное содержимое», и дочерний компонент может реагировать на него.

Вот простой пример:

// Use an empty Vue instance as an event bus
var bus = new Vue()

Vue.component('container', {
  template: '#container',

  data: function() {
    return { number: 0 }
  },

  methods: {
    increment: function() { this.number++; }
  },
  
  created: function() {
    // listen for the 'updated-content' event and react accordingly
    bus.$on('updated-content', this.increment);
  },
  
  beforeDestroy: function() {
    // Clean up
    bus.$off('updated-content', this.increment);
  }
});

var app = new Vue({
  el: '#app',

  data: { number: 0 },

  mounted: function() {
    //Update the element in the slot every second, 
    //  and emit an "updated-content" event
    setInterval(function(){ 
      this.number++;
      bus.$emit('updated-content');
    }.bind(this), 1000);
  }
});
.content, .container {
  margin: 5px;
  border: 1px solid black;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>

<template id="container">
  <div class="container">
    I am the container, and I have detected {{ number }} updates.
    <div class="content">
      <slot></slot>
    </div>
  </div>
</template>

<div id="app">

  <container>
    I am the content, and I have been updated {{ number }} times.
  </container>
  
</div>

person asemahle    schedule 01.07.2016
comment
Для первого не является DOMSubtreeModified устаревшим? - person Euan Smith; 04.07.2016
comment
Хороший улов. Я обновил ответ, чтобы использовать MutationObservers и $emit. - person asemahle; 04.07.2016
comment
@pqvst Вы правы. Старый ответ был написан для Vue 1.0, в котором использовалось ready, а не mounted. Я отредактировал ответ, чтобы вместо него использовать mounted. - person asemahle; 08.01.2018
comment
Разве VueJS по-прежнему не дает возможности привязать наблюдателя к $slots.default? Нужен ли этот сложный наблюдатель? - person stevendesu; 08.04.2018

Насколько я понимаю Vue 2+, компонент должен перерисовываться при изменении содержимого слота. В моем случае у меня был компонент error-message, который должен был скрываться до тех пор, пока у него не появилось содержимое слота для отображения. Сначала у меня был этот метод, прикрепленный к v-if в корневом элементе моего компонента (свойство computed не будет работать, Vue, похоже, не реагирует на this.$slots).

checkForSlotContent() {
    let checkForContent = (hasContent, node) => {
        return hasContent || node.tag || (node.text && node.text.trim());
    }
    return this.$slots.default && this.$slots.default.reduce(checkForContent, false);
},

Это хорошо работает, когда в слоте происходит 99% изменений, включая любое добавление или удаление элементов DOM. Единственным пограничным случаем было такое использование:

<error-message> {{someErrorStringVariable}} </error-message>

Здесь обновляется только текстовый узел, и по непонятным мне причинам мой метод не срабатывает. Я исправил этот случай, подключившись к beforeUpdate() и created(), оставив мне это для полного решения:

<script>
    export default {
        data() {
            return {
                hasSlotContent: false,
            }
        },
        methods: {
            checkForSlotContent() {
                let checkForContent = (hasContent, node) => {
                    return hasContent || node.tag || (node.text && node.text.trim());
                }
                return this.$slots.default && this.$slots.default.reduce(checkForContent, false);
            },
        },
        beforeUpdate() {
            this.hasSlotContent = this.checkForSlotContent();
        },
        created() {
            this.hasSlotContent = this.checkForSlotContent();
        }
    };
</script>
person Mathew Sonke    schedule 19.04.2019
comment
Отличное решение спасибо. Вы предложили это как функцию для добавления в VueJS или как исправление ошибки? - person Abe Petrillo; 01.03.2021

Есть еще один способ реагировать на изменения слота. Честно говоря, я нахожу это намного чище, если оно подходит. Ни emit+event-bus, ни наблюдение за мутациями мне не кажутся правильными.

Возьмем следующий сценарий:

<some-component>{{someVariable}}</some-component>

В этом случае при изменении someVariable должен реагировать какой-то компонент. Что бы я сделал здесь, так это определил ключ :key для компонента, который заставляет его повторно отображать каждый раз, когда изменяется someVariable.

<some-component :key="someVariable">Some text {{someVariable}}</some-component>

С уважением Розбех Чиряй Шарахи

person Rozbeh Sharahi    schedule 05.02.2021
comment
Красиво и просто, если вы также контролируете, где и как используется компонент, и возможно ли это. Поскольку это не обычный шаблон использования, он может быть неожиданным для потребителя компонента. У нас есть что-то более похожее на <some-component><router-view/></some-component>, поэтому шаблон, который вы используете, не будет работать. - person Euan Smith; 10.02.2021
comment
Я понимаю. В вашем случае <router-view> будет отображаемым компонентом, например MyView.vue. Если возможно, следует предпочесть генерировать событие в MyView.vue и реагировать на родителя. Пример: <some-component ref="someComponent"><router-view @someEventYouThrowInside="$refs.someComponent.someMethod()" /></some-component>. Хотя я полностью понимаю, что в некоторых сложных случаях, таких как ваш, нет способов обойти молотки, такие как EventBus или MutationObservers, просто пишу это здесь, для тех, у кого более простые случаи. С уважением - person Rozbeh Sharahi; 20.02.2021

Я бы посоветовал вам рассмотреть этот трюк: https://codesandbox.io/s/1yn7nn72rl, который Раньше я наблюдал за изменениями и делал что угодно с содержимым слотов.

Идея, вдохновленная тем, как работает компонент VIcon в vuetify, заключается в использовании функционального компонента в котором мы реализуем логику в его функции render. Объект context передается в качестве второго аргумента функции render. В частности, объект context имеет свойство data (в котором вы можете найти атрибуты, attrs) и свойство children, соответствующее слоту (вы можете вызвать функцию context.slot() с тем же результатом).

С наилучшими пожеланиями

person ekqnp    schedule 11.02.2019
comment
Мне кажется, что вы делаете что-то зависимым от содержимого - в этом случае показывается 1 или 2, если содержимое слота «foo» или «bar». Нам нужно было немного другое — реагировать на измененный графический элемент, тег img или элемент canvas. Нам нужно было что-то делать, когда это меняется, а не делать что-то, зависящее от контента. Для этого вам нужно, по сути, создать механизм просмотра контента в html. Для нас, я думаю, вышеперечисленные варианты, вероятно, проще. Однако спасибо за идею и за попытку поделиться кодом. - person Euan Smith; 12.02.2019