You will be able to navigate to the recently-created offlinecontacts/ folder and see the three automatic generated files in there.

Now let’s add some pretty standard packages (although not obligatory) just to show how they can integrate easily in this approach. In particular, the aldeed:autoform and aldeed:simple-schema packages can be a bit tricky when dealing with offline data. To style our app, let’s use meteoric (no-angular Ionic solution for Meteor). Once our routing logic is pretty straightforward, Iron Router will be our choice here. Finally, the only core package for this project is the amazing GroundDB – the piece that makes offline data possible.

Almost there! The only thing missing is to add Android as a platform for this project.

$ meteor add-platform android

Done. Plug a USB cable on your phone and make sure it’s set up for development. If everything is ok, it’s time to test it coming to life in your device. Go ahead and type:

$ meteor run android-device

After a while you will see it in your phone. Note that Iron Router will complain with you once there is no route defined. Let’s fix that and organize our app.

2. Structuring the app

Meteor gives you a lot of freedom when designing your app structure but going further on that is beyond the scope of this tutorial. Let’s make things simple instead.

Delete all files automatically generated for you inside the offlinecontacts/ folder and create four others: client/, collections/, lib/ and server/. All code inside client/ will be sent only to the client and the same logic is applied to the server. The remaining two folders are visible to both.

Create now client/css/. This folder will contain all css used in the application. In our case, we will just add the dependency required by meteoric. Create a file called app.scss and leave it there:

This is the standard meteoric markup. Currently we don’t have anything useful to display but let’s stick with it for a moment. Time to configure our router and see if everything is ok. Go to your lib/ folder and create the router.js file.

lib/router.js

JavaScript

1

2

3

4

5

6

7

Router.configure({

layoutTemplate:'layout'

});

Router.map(function(){

this.route('contacts',{path:'/'})

});

Now the router shouldn’t complain anymore. Save your files and observe the changes both on browser and mobile. You shall see an empty screen apart from a header. Nice.

3. Creating the collection

Time for doing something more specific. Let’s create a Contacts collection and a schema for it. Inside your collections/ folder, create a contacts.js file with those lines:

collections/contacts.js

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

ContactsSchema=newSimpleSchema({

name:{

type:String,

min:3,

max:20

},

email:{

type:String

},

createdAt:{

type:Date,

optional:true

},

lastUpdated:{

type:Date,

optional:true

}

});

Contacts=newMeteor.Collection('Contacts');

if(Meteor.isCordova)Ground.Collection(Contacts);

This is far more interesting and has some tricks. Let’s go over them:

Lines 1-18 define the schema thanks to the aldeed:simple-schema package. We are doing some straightforward validations (limiting the name size, for example) and defining what fields are mandatory. At first sight seems that createdAt and lastUpdated will be optional, but that’s not the case. We will see why later.

Lines 20-22 create the regular Meteor collection and ground it in the case we are running on device. This tells the app to store everything this collection sees into the local storage. But we still have to get the data and test if it will persist even if offline.

4. Handling the subscriptions

Ok, we have our collection and it’ll be grounded with we are on mobile. Now we need to subscribe to the data we’ll be displaying and grounding. Our subscriptions will be on template-level, fact that gives us more control over the readiness of our data. Create a file called contacts.js inside your client folder:

clients/contacts.js

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

Template.contacts.onCreated(function(){

varself=this;

self.autorun(function(){

if(Meteor.status().connected){

Meteor.subscribe("contacts");

};

});

});

Template.contacts.helpers({

'contacts':function(){

returnContacts.find({});

}

});

The only different thing here is the line 4. Here, we will just subscribe if connected to the server. “Why?”, you would ask me. Because you will create a new file inside client/ called init.js:

client/init.js

JavaScript

1

2

3

Meteor.startup(function(){

if(Meteor.isCordova)Meteor.subscribe("contacts");

});

And that’s the reason. If we are on the device, the subscription will be done during the startup. This of course leads to performance concerns but in order to have all data available offline you need to ground them first! So you will ground them when online and be able to play with the data whenever you want even without internet connection. Of course if you do have connection, we avoid subscribing to all the data at once.

This approach might be a huge problem for apps with large amounts of data but it’s up to you to decide if you want all the data inside your local storage. Luckly, raix (the author of the GroundDB package) already said that intelligent-subscriptions are on radar. I strongly recommend you to keep an eye to his GitHub repository and pay attention to every release.

You may have noticed that we are subscribing to a publication yet undefined. Let’s fix it. This will be on server:

server/publications.js

JavaScript

1

2

3

4

5

6

7

Meteor.publish('contacts',function(){

returnContacts.find();

});

Meteor.publish('contact',function(_id){

returnContacts.find({_id:_id});

});

We are saving some time and defining a publication for returning just one contact, which will be useful later. Don’t forget to get rid of the autopublish package that comes by default:

$ meteor remove autopublish

Last piece of puzzle is to actually render the data in our template. Go back to the contacts template of contacts.html and update the {{#ionContent}}:

clients/contacts.html

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

...

{{#ionContent}}

{{#if Template.subscriptionsReady}}

{{#ionList}}

{{#each contacts}}

{{#ionItem buttonRight=true}}

<h2>{{name}}</h2>

<p>{{email}}</p>

<button class="button button-assertive">

{{>ionIcon icon="close-circled"}}

</button>

{{/ionItem}}

{{/each}}

{{/ionList}}

{{else}}

{{>spinner}}

{{/if}}

{{/ionContent}}

...

Note the Template.subscriptionReady helper that will wait for all the subscriptions before rendering the list. I have used the sacha:spin package to render a spinner otherwise, but you can use any other that best suits you (meteoric comes with a default loader).

That sounds good. Go check your job so far. Of course you will not see any data, so open your browser console and type some dummy data like Contacts.insert({name: “Rafael”, email: “rafael@mail.com”}) and see how we are going (spare a time to appreciate the beauty of the reactivity happening on your device as well).

5. Adding and modifying data

So far we have de “R” but our app is lacking the “C”, “U” and “D”. In order to accomplish this, we will rely heavily on the autoform package – but bear in mind the idea behind would be the same regardless of which rendering engine you are using. Go back to client/contacts.html and add the following templates:

The use of <div> instead of {{#contentFor}} as the Ionic header is to avoid a overlapping bug I’ve seen in some devices. Aside from the {{> quickForm …}} clause, everything should seem straightforward. Let’s analyse then what’s is going on with it:

The schema has to be explicitly declared instead of attaching it directly to the collection. This is a feature of Collections2 package which we will not use;

Surely some fixes have to be done. First we want to remove a contact when the red square is pressed. Then the contact data should populate the edit form when any click or touch is performed on the contact. Modify clients/contacts.js and add the following:

client/contacts.js

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

...

Template.contacts.events({

'click .button-assertive':function(e){

e.preventDefault();

Meteor.call('removeContact',this._id,function(error,result){

if(error)alert(error.reason);

});

return;

}

});

Template.edit.onCreated(function(){

varself=this;

self.autorun(function(){

if(Meteor.status().connected){

Meteor.subscribe("contact",Router.current().params._id);

}

});

});

Template.edit.helpers({

'selectedDoc':function(){

returnContacts.findOne(Router.current().params._id);

}

});

The only snippet that should sound different here is the lines 3-9. There we are asynchronously calling the removeContact method passing the contact’s id as parameter. Before having a look at it, let’s handle the form. In the same file, add the following:

client/contacts.js

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

AutoForm.hooks({

insertContactForm:{

onSubmit:function(insertDoc){

Meteor.call('addContact',insertDoc,function(error,result){

if(error)alert(error.reason);

});

$(".back-button").click();

returnfalse;

}

}

});

AutoForm.hooks({

editContactForm:{

onSubmit:function(insertDoc,updateDoc,currentDoc){

varobj={_id:Router.current().params._id,updateDoc:updateDoc};

Meteor.call('editContact',obj,function(error,result){

if(error)alert(error.reason);

});

$(".back-button").click();

returnfalse;

}

}

});

According to the right way to use the normal method of autoform, we need to define a onSubmit event hook attached to the form id. The functions are pretty straightforward. They gather all the information and pass to a method (in the edit case modifying it a bit in order to ease things server-side).

The main question here is why we need this tricky attempt. Turns out that GroundDB requires us to explicitly say which methods should be cached when offline (remember that methods run both on client and server). This way we can easily write our methods and cache them to work without connection and persist even if the application shuts down.

Go to collections/contacts.js and add the following:

collections/contacts.js

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

...

Meteor.methods({

addContact:function(doc){

check(doc,ContactsSchema);

varobj={name:doc.name,email:doc.email,createdAt:newDate};

returnContacts.insert(obj);

},

editContact:function(obj){

_.extend(obj.updateDoc.$set,{lastUpdated:newDate});

check(obj._id,String);

check(obj.updateDoc.$set,ContactsSchema);

returnContacts.update({_id:obj._id},obj.updateDoc);

},

removeContact:function(id){

check(id,String);

returnContacts.remove(id);

}

});

if(Meteor.isClient){

Ground.methodResume([

'addContact',

'editContact',

'removeContact'

]);

}

This part should also bear no problem. Note in lines 4 and 11 we are validating the input against the schema defined. Lines 5 and 9 solve our problem with default values – if we are using methods in the end, let’s add the values there before saving/updating (but still we had to define it as optional otherwise the client-side validation wouldn’t let the form be submitted). Finally the lines 20-26 tells GroundDB to resume the methods in the client.

That’s pretty much all! Play around with the app (note: if you are experiencing a white screen when typing on Android, refer to this topic. It’s a bug that has been around for a while but has a simple solution, just clone the package into a packages/ folder at the root of your directory and does the changes stated). Remember that styling was not my concern in this tutorial, so the form might be a bit ugly (you may want to check the Ionic autoform package or style it better with Bootstrap).

If you are happy with your app’s functionalities, try now cutting your mobile connection off. Add, edit and remove data offline. Close the app and reopen it again to see if persists. Go online and see the sync happening. It’s alive 🙂

Note: GroundDB does the sync resolution for you but in a pretty naive way. According to the docs: “At the moment the conflict resolution is pretty basic last change recieved by server wins. This could be greatly improved by adding a proper conflict handler.” You may expect changes in the future.

You can check the repo for this app here. Clone and modify it if you want. Please feel free to give any feedback once this is more a research and any improvement/input will be highly valuable.

@import ‘.meteor/local/build/programs/server/assets/packages/meteoric_ionic-sass/ionic’;
NOTE:
Due to a current limitation of the Meteor packaging system, the above path may not exist the first time you run your Meteor app after installing this package. This will cause an error saying the file to import was not found. This may also occur if you run meteor reset. Restarting your app should fix this problem. See meteor/meteor#2606 and meteor/meteor#2796 for more info.

If I understand well, your application only works offline when compiled as an ios or android native app. But it cannot work offline as a webapp, even after using ios’ “add to main screen” feature.

If we wanted to make it work that way, I believe that it suffice to add the appcache module to the project, (https://atmospherejs.com/meteor/appcache) so that the app’s resources are stored in safari’s local storage. Do you think that’s the right way to go?

Hi Rafael,
Thanks for nice article.
I have one question. If I wanted to extend app for possibility to make a call to number in contact how could I do that? Is it difficult? Thanks in advance some hints.

Run Meteor
Comment out the
@import ‘.meteor/local/build/programs/server/assets/packages/meteoric_ionic-sass/_ionic’;
@import ‘.meteor/local/build/programs/server/assets/packages/meteoric_ionicons-sass/_ionicons’;
Let it rebuild -you should get an app with no styling
Then comment back in and the project will load correctly

I am not familiar with GroundDB yet, but I see you are using server method to save the data from the quickForm. Wouldn’t it be the same if you use the meteor way (saving the data locally and wait for data synchronization)?
I am guessing it is mandatory but I cannot explain why…

Also, Is there any reason for not using Iron Router waitOn method for data subscriptions ?

Hello Rafael.
Thanks for sharing your experience! Mobility with Meteor is not described enough. I have one question about authentication. If you have existing user in web application how to authenticate from mobile? Did you have experince?
Thanks.

When I open a browser, android and iOS app at the same time the browser works fine because it is always connected to the server, but when I disconnect the android it only shows contacts added on this device, contacts added through the browser, even if previously displayed on the android, disappear. Is this intended behaviour, why doesn’t it download the data to local db for use when offline?

The iOS device never shows any data. I think there is a bug with iOS because this happens on other apps as well, it cannot connect to the server even if I specify the -p or –mobile-server options. Can anyone help with this problem, I have tried several fixes but none of them work for me? Help greatly appreciated.

I ended up changing the autoform.addformtypes() directly, since onSubmit is not called if you use {{# autoform.. and not {{> quickForm..

Only problem persisted is right now if the app or smartphone is shut down and restarted, then some documents are missing. Now I am wondering whether to use Ground:DB Version 2 or try to solve this problem in 0.3.14?