ManuelDeLeon / viewmodel

MVVM for Meteor
https://viewmodel.org
MIT License
205 stars 23 forks source link

setting object not fields #243

Closed frankapimenta closed 8 years ago

frankapimenta commented 8 years ago

Hi Manuel,

first and foremost. Great work here. Thank you for making it available.

When looking at your contacts app code you have this:

..... if (this._id()) { this.load( Contacts.findOne(this._id()) ); } else { this.categoryId( this.selectedCategory() ); } ....

this works fine.

... but this (using Astronomy for Contacts):

Template.contact.viewmodel({ onCreated(template) { template.subscribe(contact, FlowRouter.getParam('_id')); }, onRendered(template) { let contact = Contacts.findOne(); this.load({contact: contact}) }, hasErrors() { return this.contact().getValidationErrors(); // blows up here with this.contact() undefined. } });

Do you have an idea how to make this work? I know we should wait for object subscription but in your documentation I could not find a way.

dnish commented 8 years ago

Did you try the following:

onCreated(template) {
template.subscribe(contact, FlowRouter.getParam('_id'), () => {
  let contact = Contacts.findOne();
  this.load({contact: contact})
});
},
frankapimenta commented 8 years ago

Thank you for replying that fast.

Yes, the callback in the subscribe! I don't know how I did not remember that.

By using your suggestion...

I can log this.contact() in the autorun() without errors.

If I try to log this.contact() in onRendered() and hasErrors() it throws error:

Exception from Tracker afterFlush function: debug.js:41 TypeError: this.contact is not a function

Exception in setTimeout callback: TypeError: this.contact is not a function at ViewModel.onRendered

And now I'm blocked 👎

frankapimenta commented 8 years ago

Just an extra:

-> ahead (id comes from FlowRouter)

if I try do this.load({_id: id}) instead then I can do this._id() in place of this.contact(). So I think it blows up when the object is from Astronomy.

frankapimenta commented 8 years ago

Another extra.

Considering I have a nested element Address in contact and I load it (as in your suggestion) as:

onCreated(template) { template.subscribe(contact, FlowRouter.getParam('_id'), () => { let contact = Contacts.findOne(); this.load({address: contact.raw().address}) });

it only throws viewmodel errors ( Not meteor errors) when I tried to use address().url in the template.

vm error: can't access url of undefined.

Should not the full vm instance be build before rendering the template in the browser? Is there a way to assure this.load is finished before other methods are defined in the vm instance?

dnish commented 8 years ago

Mhhh...let's do it this way:

Template.test.viewmodel({
doc: null,

onRendered(t) {
 t.subscribe("yourSub",() => {
   var contact = Contacts.findOne();
   this.doc(contact);
 });
},

autorun: [ 
  function() {
    if(this.doc()) alert('Data loaded');
  }
]

});
frankapimenta commented 8 years ago

Thank you for your help!

It works.

However when:

hasErrors() { return this.doc().getValidationErrors(); } sometimes this.doc() is null and it will fail again so I worked around by:

hasErrors() { if(!this.doc()) return new Contact(); return this.doc().getValidationErrors(); }

Would be nice if the viewmodel could have a subject (here the doc) so that the functions defined by the developer (no callbacks (autoruns and so)) would only be called in the view if subject was already loaded.

A bit like: Template.subscriptionsReady().

Is it a good idea?

frankapimenta commented 8 years ago

Funny thing is happening.

Remember:

Template.editContact.viewmodel({
doc: null,

onRendered(t) {
 const id = FlowRouter.getParam('_id');

 t.subscribe("contact", id,() => {
   var contact = Contacts.findOne();
   this.load(_.pick(contact.raw(), 'value'));
   this.doc(contact);
 });

},
editContact() {
  if(!this.doc()) {
    return new Contact(); // to avoid to have the null issue
  }
  let contact = this.doc();
  contact.set('value', this.value());
  return  contact;
},
getErrors() {
  var contact = this.editContact();
  contact.validate(false);
  console.log(contact.getValidationErrors); // put this line in mind
  return contact.getValidationErrors();
},
hasErrors() { 
  return !! this.getErrors(); 
},
getErrorValues() {
  return _.values(this.getErrors());
}
});
<template name="editContact">
 <form {{b "class: {error: hasErrors}"}}>
  {{#each getErrorValues}}
    <div class="error" {{b "text: this"}}></div>
  {{/each}}
  <input type=text {{b "value: value"}} >
</form>

After having this.doc() when rendering the template:

Moreover it runs every time the input field value of the form changes. So if the input on load show "street X" and one starts removing character by character the log runs 16 times for every character deleted.

Somehow the set of new value in this.doc()

contact.set('value', this.value());

triggers the computations and hasErrors() which will run getErrors() runs again but stops after 16 times (if validation does not fail);
When the validation fails with errors it runs forever.

Ideas? :)

dnish commented 8 years ago

Yeah, it is normally that the methods triggered if you change the value, this is how reactivity in Meteor works. Try this code:

Template.editContact.viewmodel({
doc: null,
value:null,

onRendered(t) {
 const id = FlowRouter.getParam('_id');

 t.subscribe("contact", id,() => {
   var contact = Contacts.findOne();
   this.doc(contact);
   this.value(_.pick(this.doc().raw(), 'value'));

 });

},
editContact() {
  if(!this.doc()) {
    this.doc(new Contact());
  }
  this.doc().set('value', this.value());
  return this.doc();
},
getErrors() {

    if(this.doc().validate()) {
        return {}; //Empty object, no errors...
    } else {
        return this.doc().getValidationErrors();
    }
},
hasErrors() { 
  return Object.keys(this.getErrors()).length > 0;
},
getErrorValues() {
  return _.values(this.getErrors());
}
});
frankapimenta commented 8 years ago

Thank you for the code. Yes that is how meteor works.

In your code you are not calling editContact anywhere so this.doc() is not going to ever change. I do this:

getErrors() {
    if(this. editContact().validate()) {
        return {}; //Empty object, no errors...
    } else {
        return this. editContact().getValidationErrors();
   }

and I have the same problem I explained before.

What I did was to get rid of the doc field solution.

Template.editContact.viewmodel({
value:null,
onRendered(t) {
 const id = FlowRouter.getParam('_id');

 t.subscribe("contact", id,() => {
    // wait for this.ready(); 

 });

},
autorun() { // LOOK HERE
  // I wanted for this.ready() in subscribe so contact is present for sure.
  var contact = Contacts.findOne(); // now I have reactivity
   this.value(_.pick(contact.raw(), 'value'));
},
editContact() {
  var contact = Contacts.findOne()
  if(!contact) {
    return new Contact();
  }
  contact.set('value', this.value());
  return contact;
},
getErrors() {
    var contact = this.editContact();
    if(contact.validate()) {
        return {}; //Empty object, no errors...
    }
    return contact.getValidationErrors();
},
hasErrors() { 
  return Object.keys(this.getErrors()).length > 0;
},
getErrorValues() {
  return _.values(this.getErrors());
}
});

Now reactivity works everytime I change a value via meteor mongo.

Everything now works fine with validations.

If you have any further questions do not hesitate to ask me.

ManuelDeLeon commented 8 years ago

Closing this one.