Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 130 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,100 @@
#Angularjs in Meteor
##How to use it
The angularjs app is always called meteorapp.
# Angularjs in Meteor

## Configuration

The angular HTML page will be served by meteor. The page generation can be configured in the `server.js` file. The `angularAppConfig` object is used by default:

var angularAppConfig = {
name: 'meteorapp',
appController: 'AppCtrl',
template: {
placeholderElement: '<body>',
name: 'angular.html',
locations: ['bundle/static/', 'public/'],
}

* `name` - name of the Angular app (used for `ng-app` attribute and name of app module, default: `'meteorapp'`)
* appController - name used to define the main controller on the body element (default: `'AppCtrl'`)
* template - config options for how to load and insert the Angular template

### Angular template config options

* `placeholderElement` - the tag element to substitute with the template (default: `'<body>'`')
* `name` - the name of the template file (default: `'angular.html'`)
* `locations` - relative folder locations to search for the Angular template.

Note that `locations` are set up to first try in the expected "deploy" location, then the development location.

If you define a config file `angularAppConfig.json`, it will be used for angular app configuration. Here an example of a custom config file:

{
name: 'my-meteorapp',
appController: 'AppController',
template: {
placeholderElement: '<div ng-view>',
name: 'main-view.html',
locations: ['bundle/static/', 'public/'],
}

## Configuring the Angular app

Define the main angular module, named to match the config Angular app name config setting for the meteor server (default: `'meteorapp'`):

angular.module('meteorapp', []).
config(['$routeProvider', function($routeProvider) {
$routeProvider.
when('/index', {templateUrl: 'partials/index.html', controller: MeteorCtrl}).
otherwise({redirectTo: '/'});
}]);
###Directory structure

For a complete example, see [meteor-angular-leaderboard](https://github.com/bevanhunt/meteor-angular-leaderboard)

# coffeescript example

angular.module('meteorapp', [])
.config ['$routeProvider', '$locationProvider', ($routeProvider, $locationProvider) ->
$locationProvider.html5Mode(true)
$routeProvider.when '/'
controller: 'home'
]


### Directory structure

Create the angular template file with a name and location matching your server config settings.

Default structure:

/public
/partials
angular.html(Main screen should contain body content)

###Usage
Custom structure:

/public
/partials
/angular
main-view.html(Main screen should contain body content)

The router should load the index file when you navigate to root `'/'` of your app. The index file will then be served by meteor (see server.js)

Example 'index.html' file

/client
index.html

Example index HTML file

<head>
<title>Leaderboard</title>
</head>

Meteor server will magically create the `<html>` and `<body>` element.

###Usage of $meteor

You can simply inject the $meteor module provided anywhere you want to use it, typically in your meteor-enabled controllers. Then execute the provided Meteor commands (see client.js)

app.controller('MeteorCtrl', ['$scope','$meteor',function($scope,$meteor){
$scope.todos = $meteor("todos").find({});
$meteor("todos").insert({
Expand All @@ -28,6 +108,49 @@ The angularjs app is always called meteorapp.
<button ng-click="todo.save()">Save</button>
<button ng-click="todo.remove()">Remove</button>
</div>
###Deploying
Make sure that you always write angularjs code that can be minified, else use the --debug function. To deploy with Heroku use this buildpack. Thanks to @mimah

### Current user

In order to add Meteor current user functionality:

See [current user issue #18](https://github.com/lvbreda/Meteor_angularjs/issues/18)

You can use something like:

app.directive('currentUser', function($timeout) {
return function(scope, element) {
var timeoutId; // timeoutId, so that we can cancel the user updates
// used to update the UI
function updateUser() {
element.text((Meteor.user() != null)? Meteor.user()._id : "anonymous");
}
// schedule update in one tenth of a second
function updateLater() {
// save the timeoutId for canceling
timeoutId = $timeout(function() {
updateUser(); // update DOM
updateLater(); // schedule another update
}, 100);
}
// listen on DOM destroy (removal) event, and cancel the next UI update
// to prevent updating user after the DOM element was removed.
element.bind('$destroy', function() {
$timeout.cancel(timeoutId);
});
updateLater(); // kick off the UI update process.
}
});

The `currentUser` directive is by default a HTML element attribute.

<span currentUser=""></span>

Then in a controller:

$scope.currentUser = $meteor('users').find({_id:Meteor.userId()})

### Deploying

Make sure that you always write angularjs code that can be minified, else use the `--debug` function. To deploy with Heroku use this buildpack. Thanks to @mimah

https://github.com/mimah/heroku-buildpack-meteorite
4 changes: 4 additions & 0 deletions client.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ function() {
var self = this;
self.collections = {};
self.getCollection = function(name) {
if(name == 'users'){
return Meteor.users;
}

if (self.collections[name]) {
return self.collections[name];
} else {
Expand Down
137 changes: 105 additions & 32 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,117 @@ var fs = Npm.require("fs");
var path = Npm.require("path");
var Fiber = Npm.require("fibers");


// default config
var angularAppConfig = {
name: 'meteorapp',
appController: 'AppCtrl',
template: {
placeholderElement: "<body>",
name: 'angular.html',
locations: ['bundle/static/', 'public/'],
notice: "This is used as your main page, this should contain the contents of the body.",
}
}

angularAppConfig.template.resolvedPaths = function() {
var self = this;
var resolved = [];
for (var path in this.paths) {
paths.push(path + self.name);
}
return resolved;
}

var appConfig = {
readFile: function(filePath) {
new String(fs.readFileSync(path.resolve(filePath)));
},
getAppHtml: function() {
try {
return this.readFile('bundle/app.html');
} catch(e) {
return this.readFile('.meteor/local/build/app.html');
}
},

getAngularTemplate: function() {
var templatePaths = angularApp.template.locations;

// iterate all templatePaths and try each
for (var templatePath in templatePaths)
// only attempt read if file exists
if (fs.existsSync(templatePath)
return this.readFile(templatePath);
}
console.log("Angularjs\n______\nCreate any of: " + templatePaths.join(', ') + "\n " + angularApp.template.notice);
},
replaceCode: function() {
var element = function(tag) {
return "<" + tag + " ";
}
var ngAttr = function(name, value) {
return " ng-" + name + "='" + value + "' ";
}

var loadAngularAppConfig = function() {
if (fs.existsSync('angularAppConfig.json')) {
try {
angularApp = JSON.parse(this.readFile('angularAppConfig.json'));
} catch (e) {
console.log("Angularjs\n______\nCreate a file: 'angularAppConfig.json' to override the default settings"
return null;
}
}
}

var angularApp = loadAngularAppConfig || angularAppConfig;

angularApp.template.resolvedPaths = angularAppConfig.template.resolvedPaths;

//
code = appConfig.getAppHtml();

// insert Angular template
code = code.replace(angularApp.template.placeholderElement, appConfig.getAngularTemplate());

var htmlElem = element('html');
var htmlReplacement = htmlElem + ngAttr('app', angularApp.name);

// insert ng-app on html element
code = code.replace(htmlElem, htmlReplacement);

// TODO: Allow insert on html element?
// insert ng-controller="AppCtrl" on angular placeholder element
if (angularApp.appController) {
var plh = element(angularApp.template.placeholderElement);
var plhReplacement = plh + ngAttr('controller', angularApp.appController);

code = code.replace(plh, plhReplacement);
}

if (typeof __meteor_runtime_config__ !== 'undefined') {
code = code.replace("// ##RUNTIME_CONFIG##", this.runtimeConfig());
}
}

runtimeConfig: function() {
"__meteor_runtime_config__ = " + JSON.stringify(__meteor_runtime_config__) + ";");
}
}


__meteor_bootstrap__.app
.use(connect.query())
.use(function (req, res, next) {

// Need to create a Fiber since we're using synchronous http calls
Fiber(function() {
try{
var code = fs.readFileSync(path.resolve('bundle/app.html'));
}catch(e){
var code = fs.readFileSync(path.resolve('.meteor/local/build/app.html'));
}
var angular = "";
try{
angular = fs.readFileSync(path.resolve('bundle/static/angular.html'));
}catch(e){
if(fs.existsSync("public/angular.html")){
angular = fs.readFileSync(path.resolve('public/angular.html'));
}else{
console.log("Angularjs\n______\nCreate public/angular.html\n This is used as your main page, this should contain the contents of the body.");
}
}


code = new String(code);
// console.log((new String(angular)).join());
code = code.replace("<body>",new String(angular));
code = code.replace("<html##HTML_ATTRIBUTES##>",'<html ng-app="meteorapp">');
if (typeof __meteor_runtime_config__ !== 'undefined') {
code = code.replace(
"// ##RUNTIME_CONFIG##",
"__meteor_runtime_config__ = " +
JSON.stringify(__meteor_runtime_config__) + ";");
}

var code = appConfig.replaceCode();

res.writeHead(200, {'Content-Type': 'text/html'});
res.write(code);
res.end();
return;
//next();
res.write(code);
res.end();
return;
}).run();
});