IIFE View Model seems to be undefined - javascript

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?

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

Related

Value retrived by vue-cookie won't trigger lifecycle method "updated"

I'm trying to save and read multiple json objects from my vue-application with the vue-cookie package (Version 1.1.4). The snippet below runs really well and the json object is saved and retrivied as expected.
However, I noticed as soon as the data is retrieved via cookies and i want to change data inside the object on the web page, the "updated" lifecycle method will not trigger. This behaviour is really awkward as I am only adding the cookieToJson() method to the beforMount() method. The Vue debugger also shows that the value is changed. I think the data is not reactive anymore, but how can I fix this.
data () {
return {
json: {
a: 0,
b: 1,
},
}
},
methods: {
jsonToCookie(name) {
const data = "{\"" + name + "\": " + JSON.stringify(this[name])+"}";
this.$cookie.set(name, data, { expires: '1M' }, '/app');
},
cookieToJson(name) {
const data = JSON.parse(this.$cookie.get(name));
if(data==null) return
for(var i = 0; i < Object.keys(data).length; i++) {
var name = Object.keys(data)[i]
this[name] = data[name];
}
},
beforeMount() {
console.log("beforeMount")
this.cookieToJson("json")
},
updated() {
console.log("updated")
this.jsonToCookie("json")
},
}
In case anyone run into a similar problem, I am using the localStorage now.

Change data being returned by a promise

My application has a service that is called in the controller. This service sends down an array of objects. My controller doesn't need all of the data that is being returned, instead I would like to only grab the data I need. Is there a way to construct the array of objects being returned so that I only include my needed data?
Example:
$scope.information = [];
var loadData = function() {
var promise = myService.getData();
promise.then(function(data) {
$scope.information = data.Information;
},
function (dataError) {
console.log(dataError);
)};
};
In my example above, data.Information is an array of objects that look like this:
{
id: 1,
name: 'joe',
age: '21',
hair: 'blue',
height: '70',
}
In my controller I only need the 'id' and 'name' properties, not the others. Shouldn't I want to only retrieve the necessary data? And can I construct my $scope variable so I only have this data in the objects, as to not include any unnecessary information, resulting in bloating the front end?
It looks like you want to apply a map operation to the list. That is: for each item in the list, you'd like to have every item in the list be modified in some way. You can use Array.prototype.map to accomplish this. Here is a link to the MDN docs for reference.
E.g.
$scope.information = data.Information.map(function(element) {
return {
id: element.id,
name: element.name
}
});
This should be easy - you have many options to achieve this, here's one:
$scope.information = [];
var loadData = function() {
var promise = myService.getData();
promise.then(function(data) {
$scope.information = data.Information.map(function(d) {
return {
id: d.id,
name: d.name
};
});
},
function (dataError) {
console.log(dataError);
)};
};
(I haven't tested this code so you may need to tweak it around a bit)
Tamas' answer is a good example of filtering the data AFTER it has been fetched from the 'service'. In the application I'm working on, it can sometimes expensive to calculate some of the fields of the records from our 'service', which is actually a backend server with a REST API. We wind up doing the equivalent of
var loadData = function() {
var promise = myService.getData({fields: "name,id"});
promise.then(function(data) {
$scope.information = data;
});
},
function (dataError) {
console.log(dataError);
)};
};
Then the service, or backend, does the filtering for us and the only data sent back is exactly what we need.
If you can implement (or convince the service maintainers to implement) this kind of filtering, it's another approach you can consider.
If you wish to filter the data in the service:
app.service("myService", function("$http") {
this.getData() = function() {
return $http.get(url)
.then(function(response) {
return response.data;
});
};
this.informationGet = function() {
var promise = this.getData();
var newPromise = promise.then(function(data) {
var information = data.Information.map(function(d) {
return {
id: d.id,
name: d.name
};
});
return information;
});
return newPromise;
};
});
Usage:
var promise = myService.informationGet();
promise.then(function(information) {
$scope.information = information;
},
function (dataError) {
console.log(dataError);
throw dataError;
)};
By returning data to the .then method of a promise, one creates a new promise that resolves to the value of the returned data.

Unable to hold onto variable after fetching JSON data

I am using the highstocks charting library and JQuery for this question. I am attempting to create one chart that is partitioned into three pieces, each with a different set of data. To read in the data, I am using an example from the highstock site, with the following code:
var seriesOptions = [],
names = ['MSFT', 'AAPL', 'GOOG'];
$.each(names, function(i, name) {
$.getJSON('http://www.highcharts.com/samples/data/jsonp.php?filename=' + name.toLowerCase() + '-c.json&callback=?', function(data) {
seriesOptions[i] = {
name: name,
data: data
};
});
});
After this code is processed, I use the seriesOptions variable as the series value for each of the three charts as such:
$('#container').highcharts('StockChart', {
// misc options
series: [
seriesOptions[0],
seriesOptions[1],
seriesOptions[2],
]
}
However, it seems that the seriesOptions variable is null after it comes out of the $.getJSON() method call. How can I get around this, and what is happening to the seriesOptions variable after the $.getJSON() call?
Thanks
EDIT: Specific error:
Uncaught TypeError: Cannot read property 'type' of undefined. I am pretty sure that this is referring to the seriesOptions variable, but I'll include it for clarity.
You're probably executing the highCharts call before the AJAX call has completed. You're also doing an AJAX call in a loop (albeit a small loop, it can still have the same general issues) - if you can - try and make this 1 AJAX call with all the params. If that's not an option - you can loop, and verify all the calls are done, then process:
var seriesOptions = [],
names = ['MSFT', 'AAPL', 'GOOG'];
var completedCalls = 0;
for (var i = 0; i < names.length; i++) {
var name = names[i];
$.getJSON('http://www.highcharts.com/samples/data/jsonp.php?filename=' + name.toLowerCase() + '-c.json&callback=?', function(data) {
seriesOptions.push({
name: name,
data: data
});
completedCalls++;
if (completedCalls == names.length) {
//All ajax calls done, do highcharts magic!
$('#container').highcharts('StockChart', {
// misc options
series: [
seriesOptions[0],
seriesOptions[1],
seriesOptions[2],
]
})
}
});
}
jQuery provides an interface using $.when for resolving multiple ajax calls using deferred/promise objects. An example using your configuration can be seen below.
var seriesOptions = [];
$.when(
$.getJSON('http://www.highcharts.com/samples/data/jsonp.php?filename=msft-c.json&callback=?'),
$.getJSON('http://www.highcharts.com/samples/data/jsonp.php?filename=aapl-c.json&callback=?'),
$.getJSON('http://www.highcharts.com/samples/data/jsonp.php?filename=goog-c.json&callback=?')
).done(function (r1, r2, r3) {
seriesOptions.push({
name: "MSFT",
data: r1
});
seriesOptions.push({
name: "AAPL",
data: r2
});
seriesOptions.push({
name: "GOOG",
data: r3
});
$('#container').highcharts('StockChart', {
// misc options
series: [
seriesOptions[0],
seriesOptions[1],
seriesOptions[2],
]
});
});

CanJS parseModels and data coercion

I think I may have an odd use case here. I've got a Code model with code, title, description attributes. Users are documenting work (healthcare), they enter the code, say 7, and 7 always means that something particular happened, say "The patient was cured." Whatever, doesn't matter. Point is, I don't want to bother saving the title and description in every model, but I want to be able to pull them for displaying.
So the API delivers an array of codes like [ 1, 13, "A4" ]. I'm trying to use both can.Model.parseModel and can.Map.define to coerce that array into Code models, but I'm having a hard time.
Why is parseModel, parseModels never called in this example? fiddle
Code = can.Model.extend({
parseModel: function(data) {
// return { code:data }
console.log('Never hit!');
},
parseModels: function() {
// ...
console.log('Never hit!');
}
},{
_title: can.compute(function() {
// return title from cached lookup
})
});
Model = can.Model.extend({
findAll: 'GET /Models'
},{
define: {
Codes: {
Type: Code.List
}
}
});
can.fixture('GET /Models', function() {
return [
{ Codes: [1,2,3] }, // I want to turn each number into an object
{ Codes: [4,5,6] },
{ Codes: [7,8,9] }
];
});
Model.findAll({});
.parseModels is only called during retrieval of CRUD service data.
To make your example work, you have to make a Model.parseModel convert each Code array to an an array of objects.
Alternately, you could change Model's define.Codes.Type to something like:
Codes: {
type: function(newVal){
if(newVal instanceof Code.List) {
return newVal
} else {
return new Code.List( newVal.map(function(num){ return {value: num}}) )
}
}
}

$scope.$watch ... make it return a function on another scope?

I have a watch function that returns a value for 2 series of data in Highcharts. The example below shows how this would work within the controller:
$scope.$watch("trendCompany",function(){
return $scope.chartConfig.series[0].data =
[-1.95,0.99,9.29,4.49,-1.50,-4.05,-7.05,10.13,-19.95,
2.99,-14.55,-6.15,-20.25,-27.00,-26.10,-10.95,-10.95,5.30,
6.06,11.12,10.13,11.96,22.13,6.74,4.67,1.43,0.27,4.20,-3.45,
2.10,-1.65,1.92,1.85,0.00,1.97,-5.25,-3.30,1.67,3.87,6.27,1.89,
2.27,0.59,-1.20,-5.85,-6.60,-2.25,-2.40,-2.85,-3.45,-0.15,2.63],
$scope.chartConfig.series[1].data =
[1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2];
});
$scope.chartConfig = {
options: {
chart: {
type: 'line'
}
},
series: [{
data: []
},{
data: []
}],
title: {
text: 'Hello'
},
loading: false
}
My question is.. how would
$scope.chartConfig.series[i].data be equal to another function rather than this static array? I have a function that returns a result of data like so:
//Let a propertyExpr string define a traversal route into an object
$scope.dataResult = function(obj, propertyExpr) {
return $parse(propertyExpr)(obj);
}
My first attempt was to go back to the watch function and do...
$scope.chartConfig.series[i].data = dataResult(obj, property);
Then, I tried...
$scope.chartConfig.series[i].data = function(){ return dataResult(obj,property); }
However, neither of these are working. How can I make the value of the series scope equal to something that is invoked by a function?
If you define a function on the scope you also have to call it like that.
$scope.chartConfig.series[0].data = $scope.dataResult($scope.obj, $scope.property);

Categories

Resources