I try to define some steps ( I call them pages ) in an array and each page should have the possibility to call a function on enter.
My page definition is within an array of pages and the nextHandler ( custom method ) sets the current page index and tries to call the defined function. My React class looks ( abbreviated ) like this:
var App = React.createClass({
pageDefinitions: [
{
title: "Page 1",
enterFunction: this.enterPageOne
}
],
enterPageOne: function() {
console.log("Something useful here");
},
nextHandler: function() {
var st = this.getState();
st.currentPageIndex = st.currentPageIndex + 1;
this.setState(st);
var page = this.pageDefinitions[this.state.currentPageIndex];
console.log(typeof page.enterFunction);
console.log(page.title);
if ( typeof page.enterFunction === "function") {
page.enterFunction();
}
},
getInitialState: function() {
return {
currentPageIndex = -1
};
},
render: function() {
return (
<div>Left out</div>
}
});
While title correctly prints out on console, the function is always undefined. How can I provide a function reference in my array?
Edit: As #gillesc and #justin-morgan pointed out it is a problem of scope ( this is pageDefinition, not the class )
Edit 2: Found solution, I changed pageDefinitions to getPageDefinitions() like this:
getPageDefinitions: funtion() {
var self = this;
return [
{
title: "Page 1",
enterFunction: selfenterPageOne
}
];
}
this.enterPageOne is undefined because this isn't bound to what you think it is. Try this:
var enterPageOne = function() {
console.log("Something useful here");
};
var App = React.createClass({
pageDefinitions: [
{
title: "Page 1",
enterFunction: enterPageOne
}
],
//...etc.
if you don't want a global function outside of App component, you can try this. It's not catchy but you can manage it in scope.
nextHandler: function() {
...
var page = this.pageDefinitions[this.state.currentPageIndex];
//register enterPageOne function into the array
page.enterFunction = this.enterOnePage;
console.log(typeof page.enterFunction);
console.log(page.title);
if ( typeof page.enterFunction === "function") {
page.enterFunction();
}
},
Do not define an array on class level but a function that returns an array:
getPageDefinitions: funtion() {
var self = this;
return [
{
title: "Page 1",
enterFunction: selfenterPageOne
}
];
}
Please note: As a downside , this will create every time a new array when called. This should be considered
Related
I'm new to VueJs and currently trying to load some data only once and make it globally available to all vue components. What would be the best way to achieve this?
I'm a little bit stuck because the global variables occasionally seem to become null and I can't figure out why.
In my main.js I make three global Vue instance variables:
let globalData = new Vue({
data: {
$serviceDiscoveryUrl: 'http://localhost:40000/api/v1',
$serviceCollection: null,
$clientConfiguration: null
}
});
Vue.mixin({
computed: {
$serviceDiscoveryUrl: {
get: function () { return globalData.$data.$serviceDiscoveryUrl },
set: function (newUrl) { globalData.$data.$serviceDiscoveryUrl = newUrl; }
},
$serviceCollection: {
get: function () { return globalData.$data.$serviceCollection },
set: function (newCollection) { globalData.$data.$serviceCollection = newCollection; }
},
$clientConfiguration: {
get: function () { return globalData.$data.$clientConfiguration },
set: function (newConfiguration) { globalData.$data.$clientConfiguration = newConfiguration; }
}
}
})
and in my App.vue component I load all the data:
<script>
export default {
name: 'app',
data: function () {
return {
isLoading: true,
isError: false
};
},
methods: {
loadAllData: function () {
this.$axios.get(this.$serviceDiscoveryUrl)
.then(
response => {
this.$serviceCollection = response.data;
let configurationService = this.$serviceCollection.services.find(obj => obj.key == "ProcessConfigurationService");
this.$axios.get(configurationService.address + "/api/v1/clientConfiguration").then(
response2 => {
this.$clientConfiguration = response2.data;
}
);
this.isLoading = false;
})
}
},
created: function m() {
this.loadAllData();
}
}
</script>
But when I try to access the $clientConfiguration it seems to be null from time to time and I can't figure out why. For example when I try to build the navigation sidebar:
beforeMount: function () {
let $ = JQuery;
let clients = [];
if (this.$clientConfiguration === null)
console.error("client config is <null>");
$.each(this.$clientConfiguration, function (key, clientValue) {
let processes = [];
$.each(clientValue.processConfigurations, function (k, processValue) {
processes.push(
{
name: processValue.name,
url: '/process/' + processValue.id,
icon: 'fal fa-project-diagram'
});
});
clients.push(
{
name: clientValue.name,
url: '/client/' + clientValue.id,
icon: 'fal fa-building',
children: processes
});
});
this.nav.find(obj => obj.name == 'Processes').children = clients;
The most likely cause is that the null is just the initial value. Loading the data is asynchronous so you'll need to wait for loading to finish before trying to create any components that rely on that data.
You have an isLoading flag, which I would guess is your attempt to wait for loading to complete before showing any components (maybe via a suitable v-if). However, it currently only waits for the first request and not the second. So this:
this.$axios.get(configurationService.address + "/api/v1/clientConfiguration").then(
response2 => {
this.$clientConfiguration = response2.data;
}
);
this.isLoading = false;
would need to be:
this.$axios.get(configurationService.address + "/api/v1/clientConfiguration").then(
response2 => {
this.$clientConfiguration = response2.data;
this.isLoading = false;
}
);
If it isn't that initial value that's the problem then you need to figure out what is setting it to null. That should be prety easy, just put a debugger statement in your setter:
$clientConfiguration: {
get: function () { return globalData.$data.$clientConfiguration },
set: function (newConfiguration) {
if (!newConfiguration) {
debugger;
}
globalData.$data.$clientConfiguration = newConfiguration;
}
}
Beyond the problem with the null, if you're using Vue 2.6+ I would suggest taking a look at Vue.observable, which is a simpler way of creating a reactive object than creating a new Vue instance.
Personally I would probably implement all of this by putting a reactive object on Vue.prototype rather than using a global mixin. That assumes that you even need the object to be reactive, if you don't then this is all somewhat more complicated than it needs to be.
In my split app the detail view does not bind any model.
In the component.js I instantiate a named model like this:
// creation and setup of the oData model
var oConfig = {
metadataUrlParams: {},
json: true,
defaultBindingMode : "TwoWay",
defaultCountMode : "Inline",
useBatch : false
}
// ### tab-employee ###
var oModelEmpl = new sap.ui.model.odata.v2.ODataModel("/sap/opu/odata/sap/EMP_SRV"), oConfig);
oModelEmpl.attachMetadataFailed(function() {
this.getEventBus().publish("Component", "MetadataFailedEMPL");
}, this);
this.setModel(oModelEmpl, "EMPL");
The method onSelect in der master-view controller is fired by clicking on an listitem.
onSelect: function(oEvent) {
this.showDetail(oEvent.getParameter("listItem") || oEvent.getSource());
}
This will call the method showDetail
showDetail: function(oItem) {
var bReplace = jQuery.device.is.phone ? false : true;
this.getRouter().navTo("detail", {
from: "master",
entity: oItem.getBindingContext('EMPL').getPath().substr(1),
}, bReplace);
},
In the controller of the detail-view I've these two methods for updating the binding. onRouteMatched calls bindView, where I get the error-message TypeError: oView.getModel(...) is undefined.
onRouteMatched: function(oEvent) {
var oParameters = oEvent.getParameters();
jQuery.when(this.oInitialLoadFinishedDeferred).then(jQuery.proxy(function() {
var oView = this.getView();
if (oParameters.name !== "detail") {
return;
}
var sEntityPath = "/" + oParameters.arguments.entity;
this.bindView(sEntityPath);
}, this));
},
bindView: function(sEntityPath) {
var oView = this.getView();
oView.bindElement(sEntityPath);
//Check if the data is already on the client
if (!oView.getModel().getData(sEntityPath)) {
// Check that the entity specified was found.
oView.getElementBinding().attachEventOnce("dataReceived", jQuery.proxy(function() {
var oData = oView.getModel().getData(sEntityPath);
if (!oData) {
this.showEmptyView();
this.fireDetailNotFound();
} else {
this.fireDetailChanged(sEntityPath);
}
}, this));
} else {
this.fireDetailChanged(sEntityPath);
}
},
I've tried to implement this split app relative to the template generated by WebIDE. Any idea what is missing?
As you wrote yourself, you are creating a "named Model" with the name "EMPL".
In the Controller you have to use the same name to get the Model:
this.getView().getModel("EMPL");
Likewise when calling bindElement() you have to give the model name:
// Assuming sEntityPath = "/items/0"
this.getView().bindElement("EMPL>" + sEntityPath);
I am trying to create a flux store for a React app I am building. I am using an object-assign polyfill npm package and Facebook's Flux library.
Initially I was getting the error "Cannot read property '_data' of null' error in the console which was refering to var currIds = this._data.map(function(m){return m.id;});. That method is currently the only one being called directly. I then did console.log(this) which returned "null".
I find this strange. What is going on?
My code:
var Assign = require('object-assign');
var EventEmitterProto = require('events').EventEmitter.prototype;
var CHANGE_EVENT = 'CHANGE';
var StoreMethods = {
init: function() {},
set: function (arr) {
console.log(this);
var currIds = this._data.map(function(m){return m.id;});
arr.filter(function (item){
return currIds.indexOf(item.id) === -1;
}).forEach(this.add.bind(this));
},
add: function(item){
console.log(this);
this._data.push(item);
},
all: function() {
return this._data;
},
get: function(id){
return this._data.filter(function(item){
return item.cid === id;
})[0];
},
addChangeListener: function(fn) {
this.on(CHANGE_EVENT, fn);
},
removeChangeListener: function(fn) {
this.removeListener(CHANGE_EVENT, fn);
},
emitChange: function() {
this.emit(CHANGE_EVENT);
},
bind: function(actionType, actionFn) {
if(this.actions[actionType]){
this.actions[actionType].push(actionFn);
} else {
this.actions[actionType] = [actionFn];
}
}
};
exports.extend = function(methods) {
var store = {
_data: [],
actions: {}
};
Assign(store, EventEmitterProto, StoreMethods, methods);
store.init();
require('../dispatcher').register(function(action){
if(store.actions[action.actionType]){
store.actions[action.actionType].forEach(function(fn){
fn.call(null, action.data);
})
}
});
return store;
};
I can't see where set is called, however your this can be null if the function is invoked through call (see here) or apply, and your first argument is null.
This also happens in your require.register callback:
fn.call(null, action.data) //First parameter is your 'this'.
What's the best way to set state based on the data received from observe()?
It seems setting state via componentWillMount() won't work as observe() runs after this and the data isn't available to set state.
I'm using the observe function as advised when using Parse
E.g.:
var DragApp = React.createClass({
getInitialState: function () {
return {
activeCollection : ''
};
},
observe: function() {
return {
collections: (collectionsQuery.equalTo("createdBy", currentUser))
};
},
_setactiveCollection: function(collection) {
this.setState({
activeCollection : collection
});
},
componentWillMount: function () {
var collection = this.data.collections[0];
this._setActiveCollection(collection);
},
)}
I went the wrong way about this.
I shouldn't be storing this.data into state. I can pass it into components via render.
To get round this.data not being ready before rendering, I make use of the ParseReact function pendingQueries() inside render. E.g.
if (this.pendingQueries().length > 0) {
content = 'loading...'
} else {
content = 'hello world I am' + this.data.name
}
Try:
var DragApp = React.createClass({
observe: function() {
var collections = collectionsQuery.equalTo("createdBy", currentUser);
return {
collections: collections,
activeCollection: collections[0]
};
},
render: function () {
// do something with this.data.collections and/or this.data.activeCollection
},
)}
I'm trying to create a "To Do list" in angular js. Upon completing a task, I want to be able to click on a checkbox to mark for completion. Although it is working on the index.html, I cannot get the test itself to pass.
This is my test:
describe('ToDoListController', function() {
beforeEach(module('ToDoList'));
var ctrl;
beforeEach(inject(function($controller) {
ctrl = $controller('ToDoListController');
}));
it('initialises with an empty list and term', function() {
expect(ctrl.listDisplay).toEqual([]);
expect(ctrl.taskTerm).toBeUndefined();
});
describe('when viewing the to do list', function(){
it('displays list items', function() {
ctrl.taskTerm = "hello";
ctrl.addTask();
expect(ctrl.listDisplay[0].task).toBe("hello");
});
it('is completed when clicked', function() {
ctrl.taskTerm = "hello";
ctrl.addTask();
ctrl.isCompleted("hello");
expect(ctrl.listDisplay[0].completed).toBe(true)
});
});
});
and this is my controller:
toDoList.controller('ToDoListController', [function() {
var self = this;
self.listDisplay = []
self.addTask = function() {
self.listDisplay.push({task: self.taskTerm, completed: false})
};
self.isCompleted = function(item) {
var i = self.listDisplay.indexOf(item)
self.listDisplay[i].completed = true
}
}]);
the error I'm getting back is:
TypeError: Cannot set property 'completed' of undefined
at self.isCompleted (/Users/edobrien/Documents/Projects/todo_challenge/js/toDoListController.js:13:35)
at Object.<anonymous> (/Users/edobrien/Documents/Projects/todo_challenge/test/toDoListController.spec.js:26:12)
For reference, the error is occuring at the "ctrl.isCompleted("hello");" line
Any help you guys can give would be greatly appreciated!
Your controller's isCompleted method is wrong: your list is an array of objects, no strings:
self.addTask = function() {
self.listDisplay.push({task: self.taskTerm, completed: false})
};
If you set taskTerm as "hello", the object that is pushed into the array is not the string "hello", but an object like
{task: "hello", completed: false}
So when you try to find it with indexOf("hello"), it will always return -1, because
"hello" != {task: "hello", completed: false}
Change the way you look up an object in your array:
self.isCompleted = function(item) {
var completed=false;
self.listDisplay.forEach(function (value,index) {
if (value.task ==item) {
completed=value.completed;
}
});
return completed;
};
UPDATE: I made a mistake when reading your code: isCompleted is the usual name for a method that checks if something... well, if something is completed. But actually your method sets it as completed. That kind of methods are usually named setCompleted.
So the method should be something like this:
self.setCompleted = function(item) {
self.listDisplay.forEach(function (obj,index) {
if (obj.task ==item) {
obj.completed=true; //setting it as completed
}
});
};