首页 > 解决方案 > nouislider 在某些事件中移动时消失

问题描述

这与我在这里关于 nousliders 的第一个问题非常相关:How to update div in Meteor without helper? (这个标题选得不好,因为它不是为了避免帮助)

Jankapunkt 提供的答案非常有效,例如我可以有 4 个滑块,并且重新排序不会丢失滑块状态,例如 min/max,如下所示:

切换滑块工作正常

现在我希望一些元素是非滑块,例如将 1 更改为下拉列表:

在此处输入图像描述

但是当我单击切换按钮时,1 个滑块消失(移动到下拉点的那个),并且在控制台中出现错误:

Exception in template helper: Error: noUiSlider (11.1.0): create requires a single element, got: undefined

我不明白为什么添加 if/else 会有什么不同...滑块助手正在等待准备就绪 {{#if ready}}...{{/if}} 所以它应该可以工作?任何人都明白为什么不?以及如何解决?

模板 onCreated 现在看起来像这样:

Template.MyTemplate.onCreated(function() {
  const sliders = [{
    id: 'slider-a',
    prio: 1,
    type: "NumberRange",
    options: {
      start: [0, 100],
      range: {
        'min': [0],
        'max': [100]
      },
      connect: true
    }
  }, {
    id: 'slider-b',
    prio: 2,
    type: "NumberRange",
    options: {
      start: [0, 100],
      range: {
        'min': [0],
        'max': [100]
      },
      connect: true
    }
  }, {
    id: 'dropdown-c',
    prio: 3,
    type: "Dropdown"
  }, {
    id: 'slider-d',
    prio: 4,
    type: "NumberRange",
    options: {
      start: [0, 100],
      range: {
        'min': [0],
        'max': [100]
      },
      connect: true
    }
  }, ]

  const instance = this
  instance.state = new ReactiveDict()
  instance.state.set('values', {}) // mapping values by sliderId
  instance.state.set('sliders', sliders)
})

模板现在看起来像这样,有一个 if else 语句来显示 Dropdown 或 NumberRange(滑块):

<template name="MyTemplate">
{{#each sliders}}
    <div class="range">
        <div>
            <span>id:</span>
            <span>{{this.id}}</span>
        </div>
        {{#if $eq type 'Dropdown'}}
            <select id="{{this.id}}" style="width: 200px;">
                <option value="">a</option>
                <option value="">b</option>
                <option value="">c</option>
            </select>
        {{else if $eq type 'NumberRange'}}
            <div id="{{this.id}}">
                {{#if ready}}{{slider this}}{{/if}}
            </div>
        {{/if}}
        {{#with values this.id}}
            <div>
                <span>values: </span>
                <span>{{this}}</span>
            </div>
        {{/with}}
    </div>
{{/each}}
<button class="test">Switch Sliders</button>
</template>

标签: javascriptmeteornouislider

解决方案


首先,您应该了解以下情况:

  • 您正在将经典 ui 渲染(滑块)与 Blaze 渲染(下拉菜单)混合。这会给你带来很多设计问题,下面的解决方案更像是一种 hack,而不是使用 Blaze 的 API 的干净方法
  • 由于您的组件不再只是滑块,您应该重命名变量。否则外国人很难解码你的变量的上下文。
  • 您的下拉列表当前没有保存任何值,切换按钮会重置下拉列表。
  • 按下切换按钮时,您无法破坏noUiSlider下拉列表中的 a ,从而导致上述错误。

因此,我想先给你一些关于重构代码的建议。

1.重命名变量

您可以使用 IDE 的重构功能轻松重命名所有变量名。如果您的 IDE / 编辑器中没有这样的功能,我强烈建议您启动搜索引擎来获得一个。

由于您的输入类型比滑块多,因此您应该使用更通用的名称,例如inputs表示更广泛的可能类型。

您的下拉列表中还应该有一个value条目,重新渲染时确实能够恢复上次选择状态:

Template.MyTemplate.onCreated(function () {

  const inputs = [{
    id: 'slider-a',
    prio: 1,
    type: 'NumberRange',
    options: {
      start: [0, 100],
      range: {
        'min': [0],
        'max': [100]
      },
      connect: true
    }
  }, {
    id: 'slider-b',
    prio: 2,
    type: 'NumberRange',
    options: {
      start: [0, 100],
      range: {
        'min': [0],
        'max': [100]
      },
      connect: true
    }
  }, {
    id: 'dropdown-c',
    prio: 3,
    type: 'Dropdown',
    value: '', // default none
  }, {
    id: 'slider-d',
    prio: 4,
    type: 'NumberRange',
    options: {
      start: [0, 100],
      range: {
        'min': [0],
        'max': [100]
      },
      connect: true
    }
  },]

  const instance = this
  instance.state = new ReactiveDict()
  instance.state.set('values', {}) // mapping values by sliderId
  instance.state.set('inputs', inputs)
})

现在您还必须重命名您的助手和模板助手调用:

<template name="MyTemplate">
    {{#each inputs}}
    ...
    {{/each}}
</template>

Template.MyTemplate.helpers({
  inputs () {
    return Template.instance().state.get('inputs')
  },
  ...
})

2. 处理切换事件的多种输入类型

您还应该在 switch 事件中重命名变量。此外,您需要在此处处理不同的输入类型。下拉列表没有.noUiSlider属性,它们也接收不是一个数组而是一个字符串变量作为值:

'click .test': function (event, templateInstance) {

  let inputs = templateInstance.state.get('inputs')
  const values = templateInstance.state.get('values')

  // remove current rendered inputs
  // and their events / prevent memory leak
  inputs.forEach(input => {

    if (input.type === 'Dropdown') {
      // nothing to manually remove
      // Blaze handles this for you
      return
    }

    if (input.type === 'NumberRange') {
      const target = templateInstance.$(`#${input.id}`).get(0)
      if (target && target.noUiSlider) {
        target.noUiSlider.off()
        target.noUiSlider.destroy()
      }
    }
  })

  // assign current values as
  // start values for the next newly rendered
  // inputs
  inputs = inputs.map(input => {
    const currentValues = values[input.id]

    if (!currentValues) {
      return input
    }

    if (input.type === 'Dropdown') {
      input.value = currentValues
    }

    if (input.type === 'NumberRange') {
      input.options.start = currentValues.map(n => Number(n))
    }

    return input
  }).reverse()

  templateInstance.state.set('inputs', inputs)
},

3.正确渲染/更新显示列表

现在出现了将 Blaze 渲染与经典 DOM 更新混合使用的问题:在此之前,您将遇到错误。这主要是因为现在我们的createSliders函数将期望在按下开关之前div在下拉列表已呈现的位置有一个具有特定 id 的元素。它不会在那里,因为此时 Blaze 渲染失效不会完成。

使用autorunin onCreatedor来解决这个问题onRendered很容易增加复杂性,甚至弄乱你的代码。一个更简单的解决方案是在这里使用短暂的超时:

Template.MyTemplate.helpers({
  // ...
  slider (source) {
    const instance = Template.instance()
    setTimeout(()=> {
      createSliders(source.id, source.options, instance)
    }, 50)
  },
  // ...
})

4. Bonus:保存下拉菜单的状态

为了保存下拉菜单的状态,您需要挂钩它的change事件。因此,您需要为其分配一个类以独立于id

 <select id="{{this.id}}" class="dropdown" style="width: 200px;">...</select>

现在您可以为其创建一个事件:

'change .dropdown'(event, templateInstance) {
  const $target = templateInstance.$(event.currentTarget)
  const value = $target.val()
  const targetId = $target.attr('id')
  const valuesObj = templateInstance.state.get('values')
  valuesObj[targetId] = value
  templateInstance.state.set('values', valuesObj)
}

现在您已经保存了当前的下拉值,但是为了在下一次渲染中恢复它,您需要options在 html 中扩展:

<select id="{{this.id}}" class="dropdown" style="width: 200px;">
    <option value="a" selected="{{#if $eq this.value 'a'}}selected{{/if}}">a</option>
    <option value="b" selected="{{#if $eq this.value 'b'}}selected{{/if}}">b</option>
    <option value="c" selected="{{#if $eq this.value 'c'}}selected{{/if}}">c</option>
</select>

这现在也应该显示下拉列表的最后选择状态。

概括

  • 您可以使用此模式来包含更多输入组件。
  • 请注意,将 Blaze 渲染与传统的 DOM 操作相结合会大大增加代码的复杂性。同样适用于那里的许多其他渲染系统/库/框架。
  • setTimeout当其他方法更不可行时,该解决方案应该是最后使用的方法。
  • 变量和方法命名应该始终代表它们的上下文。如果上下文发生变化 -> 重命名/重构变量和方法。
  • 请下次再次在此处发布完整代码。您的其他帖子可能会被更新或删除,并且无法再在此处访问完整的代码,这使得其他人很难找到一个好的解决方案。

推荐阅读