nouisliders dissapear when moving them on some eve

2019-08-21 09:08发布

This is very much related to my first question about nousliders here: How to update div in Meteor without helper? (this title was not well chosen, because it was not about avoiding helpers)

The answer provided by Jankapunkt works very well, for example I can have 4 sliders, and reordering works without loosing slider states like min/max like this:

switching slider works ok

now I want some of the elements to be non-sliders, for example change 1 to a dropdown:

enter image description here

but when I click the switch button, 1 slider dissapears (the one that moves to the dropdown spot), and I get an error in console:

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

I don't understand why adding if/else makes any difference ... the slider helper is waiting for ready {{#if ready}}...{{/if}} so it should work ? anyone understand why it doesn't ? and how to fix it ?

template onCreated looks now like this:

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)
})

and template now looks like this, there is an if else statement to show Dropdown or NumberRange (slider):

<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>

1条回答
时光不老,我们不散
2楼-- · 2019-08-21 09:31

First of all you should be aware of the situation:

  • You are mixing classic ui rendering (the sliders) with Blaze rendering (the dropdown). This will bring you a lot of design problems and the solution below is more a hack than a clean appraoch using Blaze's API
  • Since your components are not only sliders anymore, you should rename your variables. Otherwise it is hard for a foreign person to decode your variable's context.
  • Your dropdown has currently no values saved, switching the button resets the dropdown.
  • You can't destroy a noUiSlider on the dropdown when hitting the switch button, causing the error you described above.

Therefore I want to give you some advice on restructuring your code first.

1. Renaming variables

You can use your IDE's refactoring functionality to easily rename all your variable names. If you don't have such functionality in your IDE / editor I highly suggest you to start your search engine to get one.

Since you have more input types than sliders, you should use a more generic name, like inputs which indicates a broader range of possible types.

There should also be a value entry on your dropdown, do be able to restore the last selection state when re-rendering:

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)
})

Now you also have to rename your helpers and the template helper calls:

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

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

2. Handle multiple input types on switch event

You also should rename your variables in the switch event. Furhtermore, you need to handle different input types here. Dropdowns have no .noUiSlider property and they also receive not an array but a string variable as value:

'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. Correct rendering / update display list

Now comes the problem of mixing Blaze rendering with classic DOM updates: until this point you will run into an error. This is mainly because now our createSliders function will expect a div element with a certain id at the place where the dropdown has been rendered before the switch has been pressed. It won't be there because the Blaze render invalidation will not be finished at this point.

Fixing this using autorun in onCreated or onRendered will easily increase complexity or even messes up your code. A simpler solution is to use a short timeout here:

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

4. Bonus: saving state of dropdown

In order to save the state of the dropdown, you need to hook into it's change event. You therefore need to assign it a class to map the event independently of the id:

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

For which now you can create an event:

'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)
}

Now you have saved your current dropdown value but in order to restore it in the next render, you need to extend the options in the 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>

This should now also display the last selected state of the dropdown.

Summary

  • You can use this pattern to include even more input components.
  • Be aware, that mixing Blaze rendering with traditional DOM manipulations can increase the complexity of your code a lot. Same applies for many other rendering systems / libraries / frameworks out there.
  • The setTimeout solution should be the last approach to be used when other approaches are even less feasible.
  • Variable and method naming should always represent their context. If the context changes -> rename / refactor variables and methods.
  • Please next time post the full code here again. Your other post might get updated or deleted and the full code won't be accessible here anymore, making it hard for others to find a good solution.
查看更多
登录 后发表回答