How to implement global storage?

You have a Contact Manager tutorial. How can I implement a global storage for contacts to make it possible to get existing contact not from API, but from stored contact list?

I think this is what the tutorial does by default. The web-api.js that it gets you to download stores the contacts in an array then returns them using a timeout and Promise to simulate an HTTP fetch. This is a snippet from that file:

let contacts = [ ... ]
return new Promise(resolve => {
  setTimeout(() => {
	let results = contacts.map(x =>  ...);
	resolve(results);
  });
});

In practice, if you were caching, then this function would check to see if there was data in the cache (the cache being some local variable, or some other class injected using Aurelia’s DI) and if not make an HTTP fetch to get it. Something like this (typescript, and not tested):

export class Cache {
  public data:any = {};
  // in practice this would be more sophisticated - 
  // perhaps loading/saving from local storage, and exposing
  //  its functionality via methods rather than a property
}

@autoinject
export class WebAPI {

  constructor(private httpClient: HttpClient, private cache: Cache) {}
  
  public getContactList(): Promise {
    const cacheKey = "contact-list";
    if(this.cache[cacheKey])
      return new Promise(resolve => resolve(this.cache[cacheKey]));
    
    return this.httpClient.fetch("api/contractList")
      .then(response => response.json())
      .then(json => { 
        this.cache[cacheKey] = json;
        return new Promise(resolve => resolve(json));
      });
    });
  });
}
2 Likes

Thanks for the answer. I guess my question was not specified enough. Mostly my questions concerns global availability and bindability.
I want to require Store from any component, take its Contacts with binding. So, if any other component changes the contacts then all other components will reactively update their view without any additional work (not a pub/sub approach).

I guess the right way is to implement noview-custom-element registred in global scope and then somehow bind to this component’s data from another components. Also this noview-custom-element should be singleton.

Im not fully sure I understand your requirements but take a look at this store approach http://pragmatic-coder.net/using-a-state-container-with-aurelia/

It sounds like you don’t need any extra layer.

In your top level component, hold the contacts array model, then use two way binding to all child components.
As long as you mutate the contacts using one of monitored array methods of Aurelia ArrayObserver, the mutation will automatically trigger UI updates on all components.

FYI, Aurelia ArrayObserver monitors following methods on array mutation: pop/push/reverse/shift/sort/splice/unshift.
Assignment on the whole array also monitored, contacts = newContacts;.
But contacts[idx] = aNewContact; doesn’t work.

Mutation within one of the contact should also work (propagate to all UI).

app.js

export class App {
  contacts = []; // need to populated when app boots up.
  /* ... */
}

app.html

<template>
  <child-comp1 contacts.two-way="contacts"></child-comp1>
  <child-comp2 contacts.two-way="contacts"></child-comp2>
<template>

Remember to use two way binding on contacts all way through nested child components.

My above solution doesn’t work if using routing, which dynamically composes child component.

You are right, you need an object to hold the contacts model and related logic, but it doesn’t need to be a UI component.

Just build a class to hold contacts model, then use dependency injection on all your components. The default behavior of aurelia dependency injection is to use singleton instance on all injections which is exactly what you want.

contacts-service.js

@inject(HttpClient)
export class ContactsService {
  contacts = []; // to be populated using fetch api.

  constructor(client) {
    this.client = client;
  }

  getContact(id) { /* get a local copy of contact */}
  /*...*/
}

some-component-to-display-one-contact.js

@inject(ContactsService)
export class SomeComponentToDisplayOneContact {
  contact = null;

  constructor(contactsService) {
    this.contactsService = contactsService;
  }

  activate(params, routeConfig) {
    this.contactId = params.contactId; // assume you have contactId on the route
    this.contact = this.contactsService.getContact(this.contactId);
  }
}

I guess an alternative would be to use something like backbonejs as your api wrapping layer and subscribe to collection events in the created() methods of the aurelia view models.

Or you could just use EventAggregator

http://aurelia.io/docs/fundamentals/cheat-sheet#the-event-aggregator

@iiiyx maybe you can consider some links in this topic