0

What I did

I created a component in donejs and then created two supermodels contact and email using the following commands:

  • donejs add component contactComponent contact-component
  • donejs add supermodel contact
  • donejs add supermodel email

There's an API (feathers+mongodb) that provides contacts and emails. Each email has a contactId.

The component includes the Contact model and handles stuff like saving, creating new element, deleting an element, you name it. When combining this with the .stache file it will succesfully retrive the elements from the API and list them accordingly.

So each Contact has emails. As each contact has its own emails the contactComponent cannot directly get them but through the Contact element.

This is where my design problem starts.

So far the contactComonent creates a viewmodel that handles the way contacts are handled. The contact model handles the API connection. This works fine and is scalable and clean.

But each contact needs a new model to load data (email) then I'm direcly using the model to manage all the logic related to emails. This does work but it seems that letting the model handle the connection and a viewmodel handling complex interactions is more appropriate for an MVVM design pattern.

I guess I'm not the first person doing this type of data modeling and I think there must be a better solution especially when dealing with a larger number of relationships and more complex ones.

I think what I currently have is something like this:

contactComponent.js
├── (includes) models/contact.js
│   ├── (includes) models/email.js

And this is what I'm looking for (I might be wrong)

contactComponent.js
├── (includes) models/contact.js
├── (includes / relates / references) emailComponent.js
emailComponent.js
├── (includes) models/email.js

Used files

File structure
contactComponent
├── contactComponent.js
├── contactComponent.stache
models
├── contact.js
├── email.js
contactComponent/contactComponent.js
/* contactComponent/contactComponent.js */
import Component from 'can/component/';
import Map from 'can/map/';
import 'can/map/define/';
import template from './contactComponet.stache!';
import Contact from '../models/contact.js';


export const ViewModel = Map.extend({
  define: {
    contactPromise: {
      get: function() {
        return Contact.getList({});
      }
    }
  },
  saveContact: function() {
    // do some stuff
  },
  deleteContact: function() {
    // do some stuff
  }
});

export default Component.extend({
  tag: 'contact-component',
  viewModel: ViewModel,
  template
});
contactComponent/contactComponent.stache
/* contactComponent/contactComponent.stache */
{{#if contactPromise.isResolved}}
  {{#each contactPromise.value}}
    Name: {{name}}
    {{#if emailPromise.isResolved}}
      Emails:
      {{#each emailPromise.value}}
        {{email}}
      {{/each}}
    {{/if}}
  {{/each}}
{{/if}}
models/email.js
/* models/email.js */
import can from 'can';
import superMap from 'can-connect/can/super-map/';
import tag from 'can-connect/can/tag/';
import 'can/map/define/define';

export const Email = can.Map.extend({
  define: {},
  type: null,
  email: null,
});

Email.List = can.List.extend({
  Map: Email
}, {});

export const emailConnection = superMap({
  url: '/api/modelEmail',
  idProp: '_id',
  Map: Email,
  List: Email.List,
  name: 'email'
});

tag('email-model', emailConnection);

export default Email;

This is where stuff starts getting too complex:

models/contact.js
/* models/contact.js */
import can from 'can';
import superMap from 'can-connect/can/super-map/';
import tag from 'can-connect/can/tag/';
import 'can/map/define/define';
import Email from '../models/email.js';

export const Contact = can.Map.extend({
  define: {
    emailPromise: {
      get: function() {
        return Email.getList({ contactId: this.attr('id') });
      }
    }
  },
  name: null,
});

Contact.List = can.List.extend({
  Map: Contact
}, {});

export const contactConnection = superMap({
  url: '/api/modelContact',
  idProp: '_id',
  Map: Contact,
  List: Contact.List,
  name: 'contact'
});

tag('contact-model', contactConnection);

export default Contact;
nico
  • 144
  • 12

1 Answers1

0

There is no one right way to answer your question, but let me describe what I do.

Typically, model relations are defined at a model level. As far as I know most ORMs work this way (eg. Mongoose and Sequelize). I am one to prefer that the relationships are known on both sides - for example your Contact model knows it has many emails, and the Email model knows that it belongs to a contact. Each model can stand on it's own - meaning you don't have to have emails whenever you are dealing with a contact and vice versa.

Models can then expose helper methods for retrieving related data. So your Contact model could implement methods such as getEmails(), setEmails(), addNewEmail(), doSomethingUniqueWithEmails(). Your Email model could do the same with getContact() and setContact(). These methods would handle the actual data transactions (making AJAX calls) - so it's up to you to implement that part per your needs. For example, when you call contact.setEmails([...]) - the contact model would set the contactId on all the emails and call EmailModel.save() or something to to that end.

Finally, your viewModel would use the helper methods as needed to do business stuff. The only things your model cares about are relationships and how to persist data to the server. Your viewModels will then use business logic to determine how and when data gets created, destroyed, etc.

Hope that helps.

Ryan Wheale
  • 26,022
  • 8
  • 76
  • 96