typescriptasynchronoushtml-selectmithril.js

Using Mithril.js, how can I add options given by an async function, to a previously added select element


I'm trying to add options to a previously created Vnode (select element). The element is returned before the options are available, since they come from an async function.

Although I try to set the children upon the callback, that does not change the rendered element. And the options are not displayed in the select element.

My code bellow:

const getSelect = function (field: FieldDef, key: string) {
  const elem = m('div', { class: 'field' }, [
    m('label', { class: 'label' }, field.label ? field.label : field.name),
    m(
      'div',
      {
        class: field.controlClass
          ? 'select ' + field.controlClass
          : 'select is-small',
        onchange: (val: number | string) => {
          console.log(val);
        },
      },
      [
        m(
          'select',
          {
            class: field.class ? field.class : '',
            name: field.name ? field.name : key,
            id: key,
            value: field.value,
          },
          [],
        ),
      ],
    ),
  ]);

  if (field.options) {
    void field.options().then(function (items) {
      elem.children = [];

      elem.children.push(
        items.map((i) => {
          return m('option', { value: i.value }, i.text);
        }),
      );

      console.log('the options are:');
      console.log(elem.children);
    });
  }

  return elem;
};

Solution

  • Your code is using a mixture of expressions and statements to describe the view: something that Mithril and most Javascript view libraries since 2013 try to separate. The solution is to describe the view exclusively as an expression, by interpolating values from a model which can be changed by statements outside of the view when necessary.

    The reason your code sample won't work is that vnodes cannot be manipulated: instead new vnodes must be generated containing the appropriate data.

    The solution is to have:

    1. A dynamic data model
    2. A view expression that references the model for any dynamic values
    3. Control code which can change the model values and instruct the view to recompute

    In the code below I’ve approximated your problem case with a select element whose options will change later on. To simplify this example, the asynchronous change comes from user interaction with the view — instead of a promise like in your code — but the principles remain the same: the code that receives the new options, whatever it may be, changes the model and instructs the view to m.redraw; when the view redraws, it interpolates its persistent reference from the model, receives a new value, and persists the change to DOM.

    // 1. A dynamic data model
    const model = {
      options: [],
    }
    
    m.mount(document.body, {
      // 2. A view expression that references the model for any dynamic values
      view: () => [ 
        m('select',
          model.options.map(value => 
            m('option', value),
          ),
        ),
        
        m('hr'),
        
        m('button', {
          onclick : populate,
        },
          'Populate'
        ),
        
        
        m('button', {
          onclick : reset,
        },
          'Reset'
        ),
      ],
    })
    
    // 3. Control code which can change the model values and instruct the view to recompute
    function populate(){
      model.options = [1, 2, 3]
    
      m.redraw()
    }
    
    function reset(){
      model.options = []
    
      m.redraw()
    }
    <script src="https://unpkg.com/mithril@2.0.4/mithril.min.js"></script>