3

I'm honestly not sure why this is not working. Seems to be a pretty standard operation. It is not mounting the component, is not throwing an error, and not running the function directly after it. All happing in cfg.AddToCart.vm.addToCart()

cfg.AddToCart = {
  vm: {
    init() {
        return;
    },

    addToCart() {
        let parent = document.getElementById('atc-error');
        let errEl = document.getElementById('atc-error-component');

        if(cfg.state.selections.SIZE) {
            m.mount(errEl, null);
        } else {
            let component = new cfg.selectComponent(cfg.Options, cfg.optionsView);
            m.mount(errEl, component);

            cfg.util.toggleSlide(parent);
        }
    }
  },

  controller() {
    cfg.AddToCart.vm.init();
  }
};

cfg.AddToCart.view = function() {
  return <div id="add-to-cart-container">
    <div id="atc-error">
        <span>Select a size and add to cart again.</span>
        <div id="atc-error-component"></div>
    </div>
    <div class="small-12 columns">
        <button class="large button alert"
            onclick={() => {
                this.vm.addToCart();
            }}>
            Add To Cart
        </button>
    </div>
  </div>;
};

We use the new cfg.selectComponent(cfg.Options, cfg.optionsView) component multiple times throughout the application, so it is not an error with that. #atc-error is set to display:none, but that also doesn't seem to be the problem. This is not the only conditional mount in the application, so that is why I'm a bit stumped.

swade
  • 668
  • 2
  • 7
  • 15

2 Answers2

3

from looking at the way you've structured your code it strikes me you're missing out on a lot of Mithril's benefits. In particular:

  1. If your 'vm' is indistinguishable from the controller, then you don't need to create and manage a whole separate object for that. Especially when you're using methods to control local component state, that is the job of the controller. The controller exposes an object to the view — this should be considered the 'vm' to that extent. Having a separate object to hold model state is useful when the state is relevant outside of the component instance: you already have this in your cfg.state, so in this scenario the vm is redundant.
  2. Mithril views have a config method which exposes the real DOM element after every draw. You don't need to store references to view elements since you can do it here. This is a large part of what makes virtual DOM libraries so appealing: the view is clever, and you can introduce view-specific logic in them directly.
  3. Components can be called directly from within the view, and the view can use conditional logic to determine whether or not to call them. m.mount is only necessary to initialise a Mithril application and define 'top level' components; from within Mithril code you can invoke nested components via m function directly.

A couple of other misunderstandings:

  1. The controller executes before the view is rendered (and once it's executed, the properties it initialises are exposed to your view function as the first argument), so you can't access elements created by the view when the controller initialises.
  2. The init function in the vm serves no purpose.

Here's a rewrite of your code that takes the above into account. I used plain Mithril instead of MSX to avoid compilation, but you could easily convert it back:

// Determine what your external dependencies are
const { state, selectComponent } = cfg

// Define the component
const AddToCart = {
  // No need for a separate VM: it is identical in purpose & function to the controller
  controller : function(){
    // No need to store element references in the model: those are the view's concern.
    // Keep the VM / ctrl size to a minimum by only using it to deal with state
    this.addToCart = () => {
      if( state.selections.SIZE )
        this.showSize   = false 

      else {
        this.showSize   = true

        this.slideToErr = true 
      }
    }
  },

  view : ctrl =>
    m( '#add-to-cart-container',
      m( '#atc-error', {
        // Config exposes the element and runs after every draw.
        config : el => {
          // Observe state, and affect the view accordingly:
          if( ctrl.slideToErr ){
            el.scrollIntoView()

            // Reset the state flag
            ctrl.slideToErr = false
          }
        }
      },
        m( 'span', 'Select a size and add to cart again.' ),

        // This is an and condition, ie 'if A, then B
           ctrl.showSize 
           // This is how you invoke a component from within a view
        && m( selectComponent )
      ),

      m( '.small-12 columns',
        m( 'button.large button alert',  {
          onclick : () =>
            ctrl.addToCart();
        },
          'Add To Cart'
        )
      )
    )
}
Barney
  • 16,181
  • 5
  • 62
  • 76
  • Thanks for the detailed write up. We actually have a few ways of making components and I may have found this style from an old post. We have been wondering what the more correct way setting them up would be. I will take a very close look at this! – swade Jul 28 '16 at 13:42
0

Worked by changing it to this pattern:

cfg.AddToCart = {
  vm: {
    init() {
        this.errorComponent = m.prop();
    },

    addToCart() {
        let parent = document.getElementById('atc-error');
        let errEl = document.getElementById('atc-error-component');

        if(cfg.state.selections.SIZE) {
            cfg.util.toggleSlide(parent);
            setTimeout(() => {
                this.errorComponent(null);
            }, 400);
        } else {
            let component = new cfg.selectComponent(cfg.Options, cfg.optionsView);
            this.errorComponent(component);

            setTimeout(() => {
                cfg.util.toggleSlide(parent);
            }, 100);
        }
    }
  },

  controller() {
    cfg.AddToCart.vm.init();
  }
};

cfg.AddToCart.view = function() {
  return <div id="add-to-cart-container">
    <div id="atc-error">
        <span>Select a size and add to cart again.</span>
        <div id="atc-error-component" class="row">
            {this.vm.errorComponent() ? m.component(this.vm.errorComponent()) : ''}
        </div>
    </div>
    <div class="small-12 columns">
        <button class="large button alert"
            onclick={() => {
                this.vm.addToCart();
            }}>
            Add To Cart
        </button>
    </div>
  </div>;
};
swade
  • 668
  • 2
  • 7
  • 15