I've been learning ember by recreating the TodoMVC in EmberCli. Ive recreated all the functionality, but I ran into an issue and I was hoping someone could shed some light on the situation.
It seems that my Todos ArrayController will observe and fire functions when properties in my model change but not when values in my Todo ObjectController change.
I moved isEditing into the Model so that when I call editTodo canToggle fires. But I would prefer to store that value in my controller and not the model.
I set up a test with with a propTest boolean. on a button click I fire propTogglebut todoPropToggle doesn't respond to the change. The only time that it does ever fire is on initialization.
Any insight would be super helpful.
TODOS CONTROLLER
import Ember from 'ember';
export default Ember.ArrayController.extend({
actions: {
createTodo: function() {
var title = this.get('newTitle');
if (!title.trim()) {
return;
}
var todo = this.store.createRecord('todo', {
title: title,
isCompleted: false,
isEditing:false
});
this.set('newTitle', '');
todo.save();
}
},
canToggle: function() {
var isEditing = this.isAny('isEditing');
return this.get('length') && !isEditing;
}.property('length','#each.isEditing'),
todoPropToggle: function() {
var hasPropTest = this.isAny('propTest');
return hasPropTest;
}.property('#each.propTest')
});
TODO CONTROLLER
import Ember from 'ember';
export default Ember.ObjectController.extend({
actions: {
editTodo: function() {
var todo = this.get('model');
todo.set('isEditing', true);
},
removeTodo: function() {
var todo = this.get('model');
todo.deleteRecord();
todo.save();
},
acceptChanges: function() {
var todo = this.get('model');
todo.set('isEditing', false);
if (Ember.isEmpty(this.get('model.title'))) {
this.send('removeTodo');
}
else {
this.get('model').save();
}
},
propToggle:function(){
this.set('propTest',!this.get('propTest'));
}
},
propTest:true,
isCompleted: function(key, value) {
var model = this.get('model');
if (value === undefined) {
return model.get('isCompleted');
}
else {
model.set('isCompleted', value);
model.save();
return value;
}
}.property('model.isCompleted')
});
How about an alternative approach? We could toggle 'canToggle' in arrayController directly from the object controller by either using parentController or specifying needs. Avoids having to observer all the itemControllers which should be more efficient too.
TODOS CONTROLLER:
import Ember from 'ember';
export default
Ember.ArrayController.extend({
/**
* references the todo model that is currently being edited
*/
editingTodo: null,
canToggle: Ember.computed.notEmpty('editingTodo'),
actions: {
createTodo: function () {
var title = this.get('newTitle');
if (!title.trim()) {
return;
}
var todo = this.store.createRecord('todo', {
title: title,
isCompleted: false,
isEditing: false
});
this.set('newTitle', '');
todo.save();
}
}
});
TODO CONTROLLER
import Ember from 'ember';
export default
Ember.ObjectController.extend({
needs:['todos']
todos : Ember.computed.alias('controllers.todos'),
actions: {
editTodo: function () {
this.set('todos.editingTodo', this.get('model'));
},
removeTodo: function () {
var todo = this.get('model');
if (this.get('todos.editingTodo') === todo) {
this.set('todos.editingTodo', null);
}
todo.deleteRecord();
todo.save();
},
acceptChanges: function () {
this.set('todos.editingTodo', null);
if (Ember.isEmpty(this.get('model.title'))) {
this.send('removeTodo');
}
else {
this.get('model').save();
}
}
},
isCompleted: function (key, value) {
var model = this.get('model');
if (value === undefined) {
return model.get('isCompleted');
}
else {
model.set('isCompleted', value);
model.save();
return value;
}
}.property('model.isCompleted')
});
I have not tested it, but I hope you get the jist of the solution I am proposing.
propTest in this scenario here looks like it's a computed property, and not an observable. (edit: not that computed properties don't have underlying observables powering them, but they're different in usage enough that I like to keep them separate) It'll fire when the underlying propTest fires, but if there's no change, ember will no-op that out in the run loop. If you'd rather this be a raw observable, use the observes() syntax. #each will work here, but I like being explicit and have an observable update what it needs to rather than using the computed property, unless I need direct access to that property to bind with in a template.
The "only firing on execution" stems from the initial bindings from the computed property getting created. If it never fires after that, the underlying bindings you're using for the computed property #each.propTest must be incorrect, otherwise that would indeed fire.
I think you may also be confused to the purpose of an ObjectController. An Ember.Object can do all of the observable business that a controller can, short of having a 'model' or 'content' property backing it. It looks like you may want to go with a straight up object rather than a controller here for the todo, since ember doesn't really have a 'model' type. I'd then put the objects themselves as part of the content of the ArrayController, at which point #each would be able to iterate on them as you'd expect.
The ObjectController sits at the same level of usage as an ArrayController. You can certainly nest them as you've done here, but my spidey-sense is tingling that it's the wrong thing to do given the application. You probably don't need to have a backing controller for each todo object, you just need the todo object itself.
Related
I have a Vue component that has a vue-switch element. When the component is loaded, the switch has to be set to ON or OFF depending on the data. This is currently happening within the 'mounted()' method. Then, when the switch is toggled, it needs to make an API call that will tell the database the new state. This is currently happening in the 'watch' method.
The problem is that because I am 'watching' the switch, the API call runs when the data gets set on mount. So if it's set to ON and you navigate to the component, the mounted() method sets the switch to ON but it ALSO calls the toggle API method which turns it off. Therefore the view says it's on but the data says it's off.
I have tried to change the API event so that it happens on a click method, but this doesn't work as it doesn't recognize a click and the function never runs.
How do I make it so that the API call is only made when the switch is clicked?
HTML
<switcher size="lg" color="green" open-name="ON" close-name="OFF" v-model="toggle"></switcher>
VUE
data: function() {
return {
toggle: false,
noAvailalableMonitoring: false
}
},
computed: {
report() { return this.$store.getters.currentReport },
isBeingMonitored() { return this.$store.getters.isBeingMonitored },
availableMonitoring() { return this.$store.getters.checkAvailableMonitoring }
},
mounted() {
this.toggle = this.isBeingMonitored;
},
watch: {
toggle: function() {
if(this.availableMonitoring) {
let dto = {
reportToken: this.report.reportToken,
version: this.report.version
}
this.$store.dispatch('TOGGLE_MONITORING', dto).then(response => {
}, error => {
console.log("Failed.")
})
} else {
this.toggle = false;
this.noAvailalableMonitoring = true;
}
}
}
I would recommend using a 2-way computed property for your model (Vue 2).
Attempted to update code here, but obvs not tested without your Vuex setup.
For reference, please see Two-Way Computed Property
data: function(){
return {
noAvailableMonitoring: false
}
},
computed: {
report() { return this.$store.getters.currentReport },
isBeingMonitored() { return this.$store.getters.isBeingMonitored },
availableMonitoring() { return this.$store.getters.checkAvailableMonitoring },
toggle: {
get() {
return this.$store.getters.getToggle;
},
set() {
if(this.availableMonitoring) {
let dto = {
reportToken: this.report.reportToken,
version: this.report.version
}
this.$store.dispatch('TOGGLE_MONITORING', dto).then(response => {
}, error => {
console.log("Failed.")
});
} else {
this.$store.commit('setToggle', false);
this.noAvailableMonitoring = true;
}
}
}
}
Instead of having a watch, create a new computed named clickToggle. Its get function returns toggle, its set function does what you're doing in your watch (as well as, ultimately, setting toggle). Your mounted can adjust toggle with impunity. Only changes to clickToggle will do the other stuff.
I want to implement computed property in controller that changes when data in route's model changed.
Route:
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return new Ember.RSVP.hash({
ingredients: this.store.findAll('ingredient'),
recipes: this.store.peekAll('recipe')
});
},
setupController: function(controller, modelHash) {
controller.setProperties(modelHash);
}
});
Controller:
import Ember from 'ember';
export default Ember.Controller.extend({
pageNumber: 0,
pageSize: 16,
pages: function() {
var pages = [];
if (this.model != null) {
var content = this.model.recipes;
while (content.length > 0) {
pages.push(content.splice(0, this.get("pageSize")));
}
}
return pages;
}.property('model.recipes.#each', 'pageSize'),
recipesOnPage: function() {
return this.get('pages')[this.get('pageNumber')];
}.property('pages', 'pageNumber')
});
This code produce no error, but doesn't work - "pages" always empty. And "pages" property doesn't recomputed on model changing. What am I doing wrong? And how to achieve desired result?
P.S. Ember version - 1.13.
Since you have modified setupController hook, your controller has properties ingredients and recipes, but has no model property.
So your computed property should be:
pages: function() {
// avoid using model here
// use this.get('recipes') instead of this.model.recipes
}.property('recipes.[]', 'pageSize')
SetupController hook guides link.
Please try:
import Ember from 'ember';
export default Ember.Controller.extend({
pageNumber: 0,
pageSize: 16,
pages: function() {
var pages = [];
var model = this.get('model');
if (model != null) {
var content = model.get('recipes');
while (content.length > 0) {
pages.push(content.splice(0, this.get("pageSize")));
}
}
return pages;
}.property('model.recipes.#each', 'pageSize'),
recipesOnPage: function() {
return this.get('pages')[this.get('pageNumber')];
}.property('pages', 'pageNumber')
});
I'm using Meteor with react and FlowRouter to handle subscriptions. I find that when my component renders it will render twice after a few seconds, but only when I have the meteor mixin subscribed to a subscription.
For example:
PeoplePage = React.createClass({
displayName:"People",
mixins: [ReactMeteorData],
getMeteorData() {
const subHandles = [
Meteor.subscribe("allPeople"),
];
const subsReady = _.all(subHandles, function (handle) {
return handle.ready();
});
return {
subsReady: subsReady,
people: People.find({}).fetch(),
};
},
render(){
if(this.data.subsReady == false){
return (<Loading/>);
} else {
console.log(this.data);
........
}
The same information is shown twice. Is this due to fast render that FlowRouter uses, or is it something that I am doing incorrectly?
Hmm, I guess the problem is that you are triggering the subscription every time, when the component re-renders.. I haven't tried it, but you could check if this will solve the problem
getMeteorData() {
const subsReady = _.all(this.subs || [{}], function (handle) {
if (typeof handle.ready == 'function')
return handle.ready();
return false;
});
if (!subsReady) // you can extend it, to provide params to subscriptions
this.subs = [
Meteor.subscribe("allPeople")
];
return {
subsReady: subsReady,
people: People.find({}).fetch(),
}
}
It should not trigger the subs if they are already ready.
Be aware, that mustn't pass an empty array to _.all, because of this:
_.all([], function(a) {return a.b()}) // true
this is why I added an empty object to the array, so this way you can check for the ready member..
I would suggest doing to subscription within the componentWillMount() function. This way, you make sure that you only subscribe once before the initial render().
getMeteorData() {
var ready = _.all(this.subHandles, function (handle) {
return handle.ready();
});
return {
subsReady: ready,
people: People.find({}).fetch()
}
},
componentWillMount() {
this.subHandles = [];
this.subHandles.push(Meteor.subscribe('allPeople');
},
componentWillUnmount() {
this.subHandles.map(function(handle) {
handle.stop();
});
}
If it still renders twice, I would suggest trying to turn of fast render for the route and check if this problem still occurs.
I am new in ReactJS and "reactive programming". I tried to create a dispatcher, action and store according to this project, but I don't know how to pass data to component.
In this example it doesn't work.
var data = [1, 2, 3, 4, 5];
var AppDispatcher = Kefir.emitter();
function DataActions() {
this.getAllData = function () {
AppDispatcher.emit({
actionType: "GET_ALL"
});
};
}
var Actions = new DataActions();
var getAllDataActionsStream = AppDispatcher.filter(function (action) {
return action.actionType === "GET_ALL";
}).map(function (action) {
return function (data) {
return data;
};
});
var dataStream = Kefir.merge([getAllDataActionsStream]).scan(function (prevData, modificationFunc) {
return modificationFunc(prevData);
}, {});
var Content = React.createClass({
getInitialState: function() {
this.onDataChange = this.onDataChange.bind(this);
return {componentData: []};
},
componentDidMount: function() {
dataStream.onValue(this.onDataChange);
},
componentWillMount: function(){
dataStream.offValue(this.onDataChange);
console.log(Actions.getAllData());
},
onDataChange(newData) {
this.setState({componentData: newData});
},
render: function() {
console.log(this.state);
var list = this.state.componentData.map(function (item, i) {
return (
<li key={i}>{item}</li>
);
});
return <ul>{list}</ul>;
}
});
React.render(<Content />, document.getElementById('container'));
Before I begin to answer in length I want to answer this part up front:
but I don't know how to pass data to component.
In the example you linked the author passes in the Todos into the main component using React's props, not with an action. So that is the approach I take in my example as well.
Now here is my example. I highly reccommend looking at the example and reading along to what I've written below.
var data = [ 1, 2, 3, 4, 5 ];
// This will now log all events of the AppDispatcher in the console with the prefix 'Kefer: '
var AppDispatcher = Kefir.emitter().log("Kefir: ");
function DataActions() {
// Our application has an action of emitting a random number.
this.emitNumber = function() {
AppDispatcher.emit({
actionType: "EMIT_NUMBER"
})
};
}
var Actions = new DataActions();
var emitNumberActionStream = AppDispatcher
.filter(function(action) {
return action.actionType === "EMIT_NUMBER";
})
.map(function(action) {
console.log("EMIT_NUMBER ACTION OCCURRED!!");
return Math.floor(Math.random() * (10)) + 1;
});
// Only one stream, no need to merge right now.
//var dataStream = Kefir.merge([ getAllDataActionsStream ]);
var Content = React.createClass({
getInitialState: function() {
// Set initial componentData using the data passed into this component's via props
return { componentData: this.props.data };
},
componentDidMount: function() {
// On each emitted value run the this.onDataChange function
emitNumberActionStream.onValue(this.onDataChange);
// Every second emit a number using the Actions we created earlier
setInterval(function() {
Actions.emitNumber();
}, 1000);
},
onDataChange: function(emittedNumber) {
console.log('state on change:', this.state);
// Update the state by appending the emitted number to the current state's componentData
this.setState({ componentData: this.state.componentData.concat([emittedNumber])});
console.log('updated state: ', this.state);
console.log('-----------------');
},
render: function() {
console.log('RENDER AGAIN!');
var list = this.state.componentData.map(function(item, i) {
return (
<li key={i}>{item}</li>
);
});
return <ul>{list}</ul>;
}
})
;
// Pass in initial data using props 'data={data}'
React.render(<Content data={data}/>, document.getElementById('container'));
I modified the example you gave that wasn't working so that it works and makes a little more sense (hopefully).
The Actions and Stores work like this:
Actions:
Request a number be emitted
Stores
Listen for "EMIT_NUMBER" actions and emit a random number
And the actual component runs like this:
It gets the initial 5 numbers passed into the component via props.
Once mounted it begins listening to the store and creates a setInterval that calls the action dispatcher's emitNumber() action. The interval is to show the reactivity at work, you could imagine that there was a button to press that would call emitNumber() instead.
The store observes the action dispatcher emit "EMIT_NUMBER" and emits a number.
The component observes the store emitted a number and updates the component's state.
The component observes that its state has changed and it rerenders.
I believe the issue is that you're using ES6 syntax (which is what the example was written in... notice the Readme). You'll need to either use a transpiler like Babel or convert your method(param => console.log(param)) syntax into normal JS (ie, method(function(param) { console.log(param) });).
This is my Backbone collection:
var TodoList = Backbone.Collection.extend({
// Reference to this collection's model.
model: Todo,
url: function () {
return 'api/todos';
},
// Filter down the list of all todo items that are finished.
done: function () {
return this.filter(function (todo) { return todo.get('done'); });
},
// Filter down the list to only todo items that are still not finished.
remaining: function () {
return this.without.apply(this, this.done());
},
// We keep the Todos in sequential order, despite being saved by unordered
// GUID in the database. This generates the next order number for new items.
nextOrder: function () {
if (!this.length) return 1;
return this.last().get('order') + 1;
},
// Todos are sorted by their original insertion order.
comparator: function (todo) {
return todo.get('order');
},
addToDo: function (opts) {
var model = this;
opts.url = model.url() + '/AddToDo'
// add any additional options, e.g. a "success" callback or data
_.extend(options, opts);
return (this.sync || Backbone.sync).call(this, null, this, options);
}
});
This works fine and I can hit my URL endpoint. However, the issue is with the built in create defined in Backbone collection, the model gets automatically added to the collection once create is called. With my custom method addToDo the to-do is added successfully but I can't see it in my view unless I refresh the page.
What am I missing? Any help is appreciated!
.create is syntactic sugar around model.save and collection.add
If you're model is being created in a different way, you need to override your sync method on the model to something like:
sync: function (method, model, options) {
options || (options = {});
if (method === 'create') {
options.url = _.result(model, 'url') + '/AddToDo';
}
return Backbone.Model.prototype.sync.call(this, method, model, options);
}