Quesofat
Quesofat

Reputation: 1531

IIFE View Model seems to be undefined

I am using Mithril.JS and it looks like my vm is undefined where as prior it wasn't.

I searched around and there is very little out there in terms of mithril.js.

Code:

var app = {};

var apiData;

app.getData = function () {
  m.request({
    method: 'GET',
    url: '/api/stocks',
  }).then(function(data){
    data = apiData;
  })
};

app.App = function(data){ // model class
  this.plotCfg = {
    chart: {
        renderTo: "plot"
    },
    rangeSelector: {
        selected: 4
    },
    yAxis: {
        labels: {
            formatter: function () {
                return (this.value > 0 ? ' + ' : '') + this.value + '%';
            }
        },
        plotLines: [{
            value: 0,
            width: 2,
            color: 'silver'
        }]
    },

    plotOptions: {
        series: {
            compare: 'percent',
            showInNavigator: true
        }
    },

    tooltip: {
        pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b> ({point.change}%)<br/>',
        valueDecimals: 2,
        split: true
    },

      series: [{
          name: 'Kyle\'s Chart',
          data: apiData
      }]
  };
};
app.controller = function() { // controller
  this.apk = new app.App();
  this.cfg = this.apk.plotCfg;
};
app.plotter = function(ctrl) { // config class
  return function(elem,isin) {
      if(!isin) {
        m.startComputation();
        var chart = Highcharts.StockChart(ctrl.cfg);
        m.endComputation();
      }
  };
};
app.view = function(ctrl) { // view
  return m("#plot[style=height:400px]", {config: app.plotter(ctrl)})
};

app.Stock = function(data) {
  this.date_added = m.prop(new Date());
  this.symbol = m.prop(data.symbol);
  this.id = m.prop(data.id)
};

app.SymbolList = Array;

app.vm = (function() {
    var vm = {}
    vm.init = function() {
        //a running list of todos
        vm.list = new app.SymbolList();
        //a slot to store the name of a new todo before it is created
        app.parseData = function (data) {
          for (var i =0; i< list.length ;i++) {
            console.log(list[i].stock);
            var stockSymbol = data[i].stock;
            vm.list.push(new app.Stock({symbol : stockSymbol}));
        }
        app.parseData(apiData);
        vm.symbol = m.prop("");
        //adds a todo to the list, and clears the description field for user convenience
        vm.add = function() {
            var data = vm.symbol();
            if (vm.symbol()) {
                data = {'text': data.toUpperCase()};
                m.request({method: 'POST',
                            url: '/api/stocks',
                            data: data,
                          }).then(function(list) {
                            vm.list = [];
                            for (var i =0; i< list.length ;i++) {
                              console.log(list[i].stock);
                              var stockSymbol = list[i].stock;
                              vm.list.push(new app.Stock({symbol : stockSymbol}));
                            }
                            return;
                          })
                vm.symbol("");
            }
        };
    }
    return vm
  }
}())

app.controller2 = function() {
  app.vm.init();
}
app.view2 = function() {
  return [
      m('input', { onchange: m.withAttr('value', app.vm.symbol),  value: app.vm.symbol()}),
      m('button.btn.btn-active.btn-primary', {onclick: app.vm.add}, 'Add Stock'),
      m('ul', [
        app.vm.list.map(function(item , index) {
          return m("li", [
            m('p', item.symbol())
          ])
        })
      ])
  ]
};
m.mount(document.getElementById('chart'), {controller: app.controller, view: app.view}); //mount chart
m.mount(document.getElementById('app'), {controller: app.controller2, view: app.view2}); //mount list
<div id="app"></div>
<div id="chart"></div>
<script src="https://cdn.rawgit.com/lhorie/mithril.js/v0.2.5/mithril.js"></script>
<script src="https://code.highcharts.com/stock/highstock.js"></script>

The error that pops up in chrome is this:

app.js:119 Uncaught TypeError: Cannot read property 'init' of undefined
    at new app.controllerT (app.js:119)
    at ea (mithril.js:1408)
    at Function.k.mount.k.module (mithril.js:1462)
    at app.js:135

It was fine before I added the second mount-point, view and controller.

Any ideas?

Upvotes: 0

Views: 136

Answers (1)

Barney
Barney

Reputation: 16456

The problem is that app.vm doesn't expose init.

The current code for app.vm looks like this:

app.vm = (function(){
  var vm = {}
  vm.init = function(){
    /* lots of stuff... */

    return vm
  }
}())

This means the internal vm.init returns vm, but the app.vm IIFE doesn't return anything. It should be:

app.vm = (function(){
  var vm = {}
  vm.init = function(){
    /* lots of stuff... */
  }
  return vm
}())

It's very difficult to read your application structure because it's full of a variety of exotic patterns that don't seem to be useful. Admittedly, the 'vm' closure is a pattern introduced in Mithril guides, but I think it's far easier to write, reason about and debug applications if we avoid all these closures, initialisation calls, constructors, nested objects and namespaces.

The idea behind 'view models' comes from the state of web app development when Mithril was originally released (early 2014), when one of the principle concerns in front-end app development was a perceived lack of structure, and Mithril felt it necessary to show people how to structure objects. But structure of this form is only useful if it clarifies intent - in the code above it confuses things. For example, app.getData isn't called anywhere, and it assigns an empty global variable to its own argument before disposing of it. This kind of thing would be easier to reason about if we had less objects.

Here's the same code with some extra fixes and an alternate structure. The principles at work in this refactor:

  1. We're no longer writing any of our own constructors or closures, resulting in less dynamic code execution and avoiding potential for errors like app.vm.init
  2. We're no longer attaching things to objects unless that structure is useful or meaningful, and using simple variables or declaring things at the point of use if they're only used once, resulting in less references and less structural complexity
  3. We use object literals - var x = { y : 'z' } instead of var x = {}; x.y = 'z' so we can see holistic structures rather than having to mentally interpret code execution to work out how objects will be built at runtime
  4. Instead of using one big generic app.vm to store everything, we separate our app model into the places where they are relevant and use functions to pass values from one place to another, allowing us to split our complexity. I'll elaborate on this after showing the code:

// Model data
var seriesData = []

// Model functions
function addToSeries(data){
  seriesData.push.apply(seriesData,data)
}

function getData( symbol ){
  m.request( {method: 'POST',
    url: '/api/stocks',
    data: { text : symbol.toUpperCase() },
  } ).then(function(list) {
    return list.map(function( item ){
      return makeStock( { symbol : item.stock } )
    } )
  } )
}

function makeStock( data ) {
  return {
    date_added : new Date(),
    symbol     : data.symbol,
    id         : data.id
  }
}

// View data
var chartConfig = {
  rangeSelector: {
    selected: 4
  },
  yAxis: {
    labels: {
      formatter: function () {
        return (this.value > 0 ? ' + ' : '') + this.value + '%';
      }
    },
    plotLines: [{
      value: 0,
      width: 2,
      color: 'silver'
    }]
  },

  plotOptions: {
    series: {
      compare: 'percent',
      showInNavigator: true
    }
  },

  tooltip: {
    pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b> ({point.change}%)<br/>',
    valueDecimals: 2,
    split: true
  },

  series: [{
    name: 'Kyle\'s Chart',
    data: seriesData
  }]
}

// Components
var chartComponent = {
  view : function(ctrl) {
    return m("#plot[style=height:400px]", {
      config: function(elem,isin) {
        if(!isin)
          Highcharts.StockChart(elem, chartConfig)
      }
    })
  }
}

var todosComponent = {
  controller : function(){
    return {
      symbol : m.prop('')
    }
  },

  view : function( ctrl ){
    return [
      m('input', {
        onchange: m.withAttr('value', ctrl.symbol),
        value: ctrl.symbol()
      }),

      m('button.btn.btn-active.btn-primary', {
        onclick: function(){
          if( ctrl.symbol() )
            getData( ctrl.symbol() )
              .then( function( data ){
                addToSeries( data )
              } )

          ctrl.symbol('')
        }
      }, 'Add Stock'),

      m('ul',
        todos.map(function(item) {
          return m("li",
            m('p', item.symbol)
          )
        })
      )
    ]
  }
}

// UI initialisation
m.mount(document.getElementById('chart'), chartComponent)
m.mount(document.getElementById('app'), todosComponent)

There is no more app, or vm, or list. These end up being unhelpful because they're so vague and generic they get used to store everything - and when one object contains everything, you may as well have those things freely available.

The core dynamic data list is now called seriesData. It's just an array. In order to interact with it, we have 3 simple functions for mutating the series data, fetching new data, and creating a new data point from input. There's no need for constructors here, and no need for props - props are a Mithril utility for being able to conveniently read and write data from an input - they're incompatible with the Highcharts API in any case.

That's all the model data we need. Next we have the code specific to our UI. The Highcharts config object references seriesData, but apart from that its an esoteric object written to conform with Highcharts API. We leave out renderTo, because that's determined dynamically by our Mithril UI.

Next comes the components, which we write as object literals instead of piecing them together later - a component controller only makes sense in relation to its view. The chartComponent doesn't actually need a controller, since it has no state and just reads previously defined model and view data. We supply the element reference directly to the Highcharts API in the config function. Because this function is only used once in a single place, we declare it inline instead of defining it in one place and binding it somewhere else. start/endComputation are unnecessary since the process is synchronous and there's no need to stop Mithril rendering during this process.

I couldn't quite work out how the 'todos' model was meant to work, but I assumed that the second component is designed to provide an alternate view of data points and allow user input to define and fetch more data. We store the 'symbol' prop in the controller here, since it's a stateful property that's used exclusively by the view. It's our only stateful property relating to this component, so that's all we define in the controller. Earlier on we simplified the model-related functions - now in the view we interact with these, passing in the symbol data explicitly instead of defining it elsewhere and retrieving it in another place. We also reset the value here, since that's an aspect of this component's logic, not the overal data model.

Upvotes: 3

Related Questions