AngularJS compile a template and use it in Showdown extension - angularjs

I'm attempting to create a Showdown extension in my Angular app which will show scope variables. I was able to get it setup to show basic scope variables easily enough, but now I'd like to get it to where I can use the results of an ng-repeat and I can't get anything other than [[object HTMLUListElement]] to show.
Here's my controller so far:
app.controller('MyCtrl', ['$scope', '$window', '$compile', function($scope, $window, $compile){
$scope.machines = [
{ abbv: 'DNS', name: 'Did Not Supply' },
{ abbv: 'TDK', name: 'The Dark Knight' },
{ abbv: 'NGG', name: 'No Good Gofers'}
];
$scope.machine = $scope.machines[0];
$scope.machine_list = $compile('<ul><li ng-repeat="m in machines">{{m.abbv}}: {{m.name}}</li></ul>')($scope);
$scope.md = "{{ machine_list }}";
var scopevars = function(converter) {
return [
{ type: 'lang', regex: '{{(.+?)}}', replace: function(match, scope_var){
scope_var = scope_var.trim();
return $scope.$eval(scope_var);
}}
];
};
// Client-side export
$window.Showdown.extensions.scopevars = scopevars;
}]);
Plunkr: code so far
I feel like I've got to be close, but now I don't know if I'm on the completely wrong track for this, or if it's a showdown thing or an angular thing or what.

I realized I was fighting Angular (and losing quiet badly) with the way I was doing that. DOM in a controller is a no-no. And now I'm kind of angry about how easy it is once I started thinking properly and looking at the directive.
Now instead of trying to do the compile and everything within the controller, I included the $compile service in the directive I'm using, so:
htmlText = converter.makeHtml(val);
element.html(htmlText);
became:
htmlText = converter.makeHtml(val);
element.html(htmlText);
$compile(element.contents())(scope);
With that change in place I no longer need the portion of the extension that just did the basic evaluation, since it's being compiled {{ machine.name }} is automatically converted.
But that still left me not being able to specify a variable to insert a template, just variables. But now that the output is going to be compiled through angular I can put the template in a partial and use an extension to convert from a pattern to an ng-include which works.
New Controller Code:
app.controller('MyCtrl', ['$scope', '$window', '$compile', function($scope, $window, $compile){
$scope.machines = [
{ abbv: 'DNS', name: 'Did Not Supply' },
{ abbv: 'TDK', name: 'The Dark Knight' },
{ abbv: 'NGG', name: 'No Good Gofers'},
{ abbv: 'TotAN', name:'Tales of the Arabian Nights'}
];
$scope.machine = $scope.machines[0];
$scope.tpls = {
'machinelist': 'partials/ml.html'
};
$scope.md = "{{machines.length}}\n\n{{include machinelist}}";
var scopevars = function(converter) {
return [
{ type: 'lang', regex: '{{include(.+?)}}', replace: function(match, val){
val = val.trim();
if($scope.tpls[val] !== undefined){
return '<ng-include src="\''+$scope.tpls[val]+'\'"></ng-include>';
} else {
return '<pre class="no-tpl">no tpl named '+val+'</pre>';
}
}}
];
};
// Client-side export
$window.Showdown.extensions.scopevars = scopevars;
}]);
And of course the new plunkr
Hope this can help someone later down the road

Related

AngularJs Typeahead directive not working

I have a simple requirement wherein a list of users is displayed and display a search button on top to search for the users by name, something like a simplified LinkedIn Connections page.
My web app is developed on node.js but this one page has been developed on angular.js and for this search button, I have decided to use the typeahead directive. This is how the jade file looks like:
html(ng-app='geniuses')
head
title List All Geniuses!
link(href='//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css', rel='stylesheet')
script(src='http://ajax.googleapis.com/ajax/libs/angularjs/1.3.10/angular.min.js')
script(src="https://cdn.firebase.com/js/client/2.2.4/firebase.js")
script(src="https://cdn.firebase.com/libs/angularfire/1.1.2/angularfire.min.js")
script(src='js/listAllgeniuses.js')
body
div.container
div.page-header
h2 All Geniuses!
div(ng-app='geniuses',ng-controller='SearchAGenius')
input.form-control(placeholder='Genius',name='search-genius',ng-model="selected",typeahead="user for user in usersArr | filter:{'geniusid':$viewValue} | limitTo:8")
div(ng-app='geniuses',ng-controller='GetAllGeniuses')
ul
li(ng-repeat='user in users') {{ user.geniusid }}
The list of users are being fetched as an array from firebase. As you can see, the list of users is fetched using GetAllGeniuses controller and it works fine.. Here is the controller code:
(function (angular) {
var app = angular.module('geniuses', ["firebase"]);
app.controller('GetAllGeniuses', ["$scope", "$rootScope","$firebaseArray",
function($scope, $rootScope, $firebaseArray) {
var users = $firebaseArray(new Firebase("****));
$rootScope.usersArr = users;
$scope.users = users;
}
])
app.controller('SearchAGenius', ["$scope", "$rootScope",
function($scope, $rootScope) {
$scope.selected = '';
$scope.usersArr = $rootScope.usersArr;
}
])
}(angular));
This is how the data looks like(dummy):
[
{
geniusid: "new",
geniusname: ""
},
{
geniusid: "new",
geniusname: ""
},
{
geniusid: "news",
geniusname: ""
},
{
geniusid: "qazwsx",
geniusname: ""
}
]
I want to search using the geniusid (or name) in the search box... I have tried almost all ideas posted on the net but haven't been able to figure this out..
Any ideas would be appreciated.
Check out this Plunker I made using your demo.
A few things to note. You'll want to include Angular Bootstrap in your scripts and inject it into your module.
script(src='http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.13.0.min.js')
And
var app = angular.module('geniuses', ["firebase","ui.bootstrap"]);
Also, don't use $rootScope to pass data around. This is a prefect use for an angular service.
There's also no need to define ng-app everytime you're going to use angular.
Here's the rest of the plunker code that I modified to get this working.
html(ng-app='geniuses')
head
title List All Geniuses!
link(href='//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css', rel='stylesheet')
script(src='http://ajax.googleapis.com/ajax/libs/angularjs/1.3.10/angular.min.js')
script(src='http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.13.0.min.js')
script(src="https://cdn.firebase.com/js/client/2.2.4/firebase.js")
script(src="https://cdn.firebase.com/libs/angularfire/1.1.2/angularfire.min.js")
script(src="./app.js")
body
div.container
div.page-header
h2 All Geniuses!
div(ng-controller='SearchAGenius')
input.form-control(placeholder='Genius',name='search-genius',ng-model="selected",typeahead="user as user.geniusname for user in usersArr | filter:{'geniusid':$viewValue} | limitTo:8")
div(ng-controller='GetAllGeniuses')
ul
li(ng-repeat='user in users') {{ user.geniusid }}
And the JS
(function(angular) {
var app = angular.module('geniuses', ["firebase", "ui.bootstrap"]);
app.controller('GetAllGeniuses', ["$scope", 'GeniusFactory',
function($scope, GeniusFactory) {
$scope.users = GeniusFactory.users();
}
]);
app.controller('SearchAGenius', ["$scope", 'GeniusFactory',
function($scope, GeniusFactory) {
$scope.selected = '';
$scope.usersArr = GeniusFactory.users();
}
]);
app.factory('GeniusFactory', ["$firebaseArray", function($firebaseArray) {
//Create a users object
var _users;
return {
users: users
}
function users() {
//This will cache your users for as long as the application is running.
if (!_users) {
//_users = $firebaseArray(new Firebase("****"));
_users = [{
geniusid: "new",
geniusname: "Harry"
}, {
"geniusid": "new",
"geniusname": "Jean"
}, {
"geniusid": "news",
"geniusname": "Mike"
}, {
"geniusid": "qazwsx",
"geniusname": "Lynn"
}];
}
console.log(_users);
return _users;
}
}]);
})(angular);

AngularJS: Trying to make filter work with delayed data

I have this html:
<div ng-app='myApp'>
<div ng-controller='testCtrl'>
<h3>All possesions</h3>
{{possessions}}
<h3>Green cars</h3>
{{greenishCars}}
</div>
</div>
And this script:
angular.module('myApp', [])
.factory('getPossessionsService', ['$timeout',
function($timeout) {
var possessions = {};
$timeout(function() {
possessions.cars = [{
model: "Mazda 6",
color: "lime green"
}, {
model: "Audi A3",
color: "red"
}, {
model: "Audi TT",
color: "green"
}, {
model: "Volkswagen Lupo",
color: "forest green"
}];
possessions.jewelry = [{
type: "ring",
metal: "gold"
}, {
type: "earring",
metal: "silver"
}];
}, 1000);
return possessions;
}
])
.controller('testCtrl', ['$scope', 'filterFilter', 'getPossessionsService',
function($scope, filterFilter, getPossessionsService) {
$scope.possessions = getPossessionsService;
$scope.greenishCars = filterFilter($scope.possessions.cars, carColorIsGreenShade);
function carColorIsGreenShade(car) {
return ['green', 'forest green', 'lime green'].indexOf(car.color) != -1;
}
}
]);
I am trying to get $scope.greenishCars to update correctly when the data is available, but it is not. I understand that this has because $scope.possessions.cars is an array and therefore not a reference to the data, so it is not updated. But how should I alter my script so that greenishCars get updated when the data arrives? I am guessing I should use $scope.possessions "directly", but I do not quite see how I should rewrite this nicely....
See this plunk.
Edit: Thoughts on which answer to choose
As in comments in the answers to Oliver and MajoB, I ended up with using both filter and watch. In my special case the request for the data was made in another place (not in the controller of my page in question), so it was not so easy to act on the resolving of the promise (with promise.then as suggested by Oliver), I therefore used a watch. But there is a couple of things to be aware of with watches. If the variable you want to watch is not on the scope, then you must provide the variable by returning it from a function. And if you want to watch for a change in an existing property (say somebody repaints my existing Mazda 6 in a different color), then none of the watch-answers works, as you need to add "true" when calling $watch. When you add 'true' as the second paramenter to $watch, then it watches for changes in the actual values of the variable, and it also does this check for values deeply in an object/array (without it just checks references, see this blog). I ended up with this controller/$watch for my real-life use-case (which is a little bit different that in my example above, and is based on Olivers filter-plunk), and it looks like this (changed to fit the plunk):
.controller('testCtrl', ['$scope', 'greenFilter', 'getPossessionsService',
function($scope, greenFilter, getPossessionsService) {
var none-scope-possessions = getPossessionsService; // I do not want to expose all properties on the scope....
$scope.greenishCars = [];
$scope.$watch(function() {return none-scope-possessions.cars}, function(newValue){
if (newValue) {
$scope.greenishCars = greenFilter(newValue);
}
},true);
}
]);
You can watch the data changes:
$scope.$watch('possessions.cars', function(){
$scope.greenishCars = filterFilter($scope.possessions.cars, carColorIsGreenShade);
});
I would recommend just building your own custom filter like so:
.filter('green', function() {
return function(possessions) {
if (!possessions) return null;
var filtered = [];
angular.forEach(possessions, function(possesion){
if (['green', 'forest green', 'lime green'].indexOf(possesion.color) != -1) {
filtered.push(possesion);
}
});
return filtered;
};
})
Then pass your collection through that filter like so:
<div ng-controller='testCtrl'>
<h3>All possesions</h3>
{{possessions}}
<h3>Green cars</h3>
{{possessions.cars|green}}
</div>
Everything else will happen automatically. See the updated plunkr.
You can watch possesions collection for changes:
$scope.$watchCollection('possessions', function (newValue) {
if (newValue)
{
$scope.greenishCars = filterFilter(newValue.cars, carColorIsGreenShade);
}
});
http://plnkr.co/edit/Sl6dW0pqKr593OXrgDb9?p=preview
First thing is , var possessions = {}; you have declared an object , cars is an field in this object , which has array of cars . so either return possessions.cars or in the markup call it via possessions.cars[0].. if you want to call only one value ..or if you want to display all the values the use ng-repeat=" car in possessions.cars" .

Service - Cannot read property of undefined

I am trying to put together a service for my controllers. But I keep getting an error saying
"Cannot read property 'industriesArr' of undefined". I'm pretty sure I'm misunderstanding something fundamental, so please educate me.
Service:
angular.module('core').service('FormService', function() {
var data = {
'industriesArr': [
'Alcoholic Drinks','Animals and Pets','Arts and Entertainment',
'Baby and Toddler','Banking','Beauty and Personal Care','Building and Construction',
'Clothing and Footwear','Communication Services','Confectionary and Snacks',
'Dining and Nightlife',
'Education and Jobs','Electronics and Technology',
'Family and Community','Fast food and Restaurant','Financial Services','Food and Groceries',
'Games and Toys','Government and Law',
'Health','Home and Garden',
'Insurances','Internet, Telecom and Software',
'Jewelry and Luxury',
'Leisure',
'Media and Publications',
'News','Non-alcoholic Drinks','Non-profit Organisation',
'Occasions and Gifts','Office Supplies','Other',
'Personal Accessories','Pharmaceutical and Medical','Political Organisation','Professional Services','Public Interest',
'Real Estate','Retail Services and Wholesaler',
'Sports Accessories','Sports and Fitness',
'Tobacco','Tourism and Travel','Transport',
'Utilities',
'Vehicles'
]
};
return data;
});
Controller:
angular.module('core').controller('SignupController', ['$scope', 'FormService', function($scope, FormService) {
$scope.industriesArr = FormService.data.industriesArr;
});]);
HTML is simply:
<p ng-repeat="industry in industriesArr">{{industry}}</p>
The way you are returning data you should use factory. So instead of this
angular.module('core').service('FormService', function() {
Use this
angular.module('core').factory('FormService', function() {
When the service syntax is used Angular treats it as Constructor function. If you want to use service do not do return and define your properties\functions on this such as
this.industriesArr=[...];
Write your module like this :
angular.module('core').service('FormService', function() {
return{
getData: function() {
return { 'industriesArr': [...]; }
}
}
});
Made a silly mistake and put FormService in the incorrect order when associating it with my controller.

Proper place for data-saving logic in AngularJS

App design question. I have a project which has a very large number of highly customized inputs. Each input is implemented as a directive (and Angular has made this an absolute joy to develop).
The inputs save their data upon blur, so there's no form to submit. That's been working great.
Each input has an attribute called "saveable" which drives another directive which is shared by all these input types. the Saveable directive uses a $resource to post data back to the API.
My question is, should this logic be in a directive at all? I initially put it there because I thought I would need the saving logic in multiple controllers, but it turns out they're really happening in the same one. Also, I read somewhere (lost the reference) that the directive is a bad place to put API logic.
Additionally, I need to introduce unit testing for this saving logic soon, and testing controllers seems much more straightforward than testing directives.
Thanks in advance; Angular's documentation may be… iffy… but the folks in the community are mega-rad.
[edit] a non-functional, simplified look at what I'm doing:
<input ng-model="question.value" some-input-type-directive saveable ng-blur="saveModel(question)">
.directive('saveable', ['savingService', function(savingService) {
return {
restrict: 'A',
link: function(scope) {
scope.saveModel = function(question) {
savingService.somethingOrOther.save(
{id: question.id, answer: question.value},
function(response, getResponseHeaders) {
// a bunch of post-processing
}
);
}
}
}
}])
No, I don't think the directive should be calling $http. I would create a service (using the factory in Angular) OR (preferably) a model. When it is in a model, I prefer to use the $resource service to define my model "classes". Then, I abstract the $http/REST code into a nice, active model.
The typical answer for this is that you should use a service for this purpose. Here's some general information about this: http://docs.angularjs.org/guide/dev_guide.services.understanding_services
Here is a plunk with code modeled after your own starting example:
Example code:
var app = angular.module('savingServiceDemo', []);
app.service('savingService', function() {
return {
somethingOrOther: {
save: function(obj, callback) {
console.log('Saved:');
console.dir(obj);
callback(obj, {});
}
}
};
});
app.directive('saveable', ['savingService', function(savingService) {
return {
restrict: 'A',
link: function(scope) {
scope.saveModel = function(question) {
savingService.somethingOrOther.save(
{
id: question.id,
answer: question.value
},
function(response, getResponseHeaders) {
// a bunch of post-processing
}
);
}
}
};
}]);
app.controller('questionController', ['$scope', function($scope) {
$scope.question = {
question: 'What kind of AngularJS object should you create to contain data access or network communication logic?',
id: 1,
value: ''
};
}]);
The relevant HTML markup:
<body ng-controller="questionController">
<h3>Question<h3>
<h4>{{question.question}}</h4>
Your answer: <input ng-model="question.value" saveable ng-blur="saveModel(question)" />
</body>
An alternative using only factory and the existing ngResource service:
However, you could also utilize factory and ngResource in a way that would let you reuse some of the common "saving logic", while still giving you the ability to provide variation for distinct types of objects / data that you wish to save or query. And, this way still results in just a single instantiation of the saver for your specific object type.
Example using MongoLab collections
I've done something like this to make it easier to use MongoLab collections.
Here's a plunk.
The gist of the idea is this snippet:
var dbUrl = "https://api.mongolab.com/api/1/databases/YOURDB/collections";
var apiKey = "YOUR API KEY";
var collections = [
"user",
"question",
"like"
];
for(var i = 0; i < collections.length; i++) {
var collectionName = collections[i];
app.factory(collectionName, ['$resource', function($resource) {
var resourceConstructor = createResource($resource, dbUrl, collectionName, apiKey);
var svc = new resourceConstructor();
// modify behavior if you want to override defaults
return svc;
}]);
}
Notes:
dbUrl and apiKey would be, of course, specific to your own MongoLab info
The array in this case is a group of distinct collections that you want individual ngResource-derived instances of
There is a createResource function defined (which you can see in the plunk and in the code below) that actually handles creating a constructor with an ngResource prototype.
If you wanted, you could modify the svc instance to vary its behavior by collection type
When you blur the input field, this will invoke the dummy consoleLog function and just write some debug info to the console for illustration purposes.
This also prints the number of times the createResource function itself was called, as a way to demonstrate that, even though there are actually two controllers, questionController and questionController2 asking for the same injections, the factories get called only 3 times in total.
Note: updateSafe is a function I like to use with MongoLab that allows you to apply a partial update, basically a PATCH. Otherwise, if you only send a few properties, the entire document will get overwritten with ONLY those properties! No good!
Full code:
HTML:
<body>
<div ng-controller="questionController">
<h3>Question<h3>
<h4>{{question.question}}</h4>
Your answer: <input ng-model="question.value" saveable ng-blur="save(question)" />
</div>
<div ng-controller="questionController2">
<h3>Question<h3>
<h4>{{question.question}}</h4>
Your answer: <input ng-model="question.value" saveable ng-blur="save(question)" />
</div>
</body>
JavaScript:
(function() {
var app = angular.module('savingServiceDemo', ['ngResource']);
var numberOfTimesCreateResourceGetsInvokedShouldStopAt3 = 0;
function createResource(resourceService, resourcePath, resourceName, apiKey) {
numberOfTimesCreateResourceGetsInvokedShouldStopAt3++;
var resource = resourceService(resourcePath + '/' + resourceName + '/:id',
{
apiKey: apiKey
},
{
update:
{
method: 'PUT'
}
}
);
resource.prototype.consoleLog = function (val, cb) {
console.log("The numberOfTimesCreateResourceGetsInvokedShouldStopAt3 counter is at: " + numberOfTimesCreateResourceGetsInvokedShouldStopAt3);
console.log('Logging:');
console.log(val);
console.log('this =');
console.log(this);
if (cb) {
cb();
}
};
resource.prototype.update = function (cb) {
return resource.update({
id: this._id.$oid
},
angular.extend({}, this, {
_id: undefined
}), cb);
};
resource.prototype.updateSafe = function (patch, cb) {
resource.get({id:this._id.$oid}, function(obj) {
for(var prop in patch) {
obj[prop] = patch[prop];
}
obj.update(cb);
});
};
resource.prototype.destroy = function (cb) {
return resource.remove({
id: this._id.$oid
}, cb);
};
return resource;
}
var dbUrl = "https://api.mongolab.com/api/1/databases/YOURDB/collections";
var apiKey = "YOUR API KEY";
var collections = [
"user",
"question",
"like"
];
for(var i = 0; i < collections.length; i++) {
var collectionName = collections[i];
app.factory(collectionName, ['$resource', function($resource) {
var resourceConstructor = createResource($resource, dbUrl, collectionName, apiKey);
var svc = new resourceConstructor();
// modify behavior if you want to override defaults
return svc;
}]);
}
app.controller('questionController', ['$scope', 'user', 'question', 'like',
function($scope, user, question, like) {
$scope.question = {
question: 'What kind of AngularJS object should you create to contain data access or network communication logic?',
id: 1,
value: ''
};
$scope.save = function(obj) {
question.consoleLog(obj, function() {
console.log('And, I got called back');
});
};
}]);
app.controller('questionController2', ['$scope', 'user', 'question', 'like',
function($scope, user, question, like) {
$scope.question = {
question: 'What is the coolest JS framework of them all?',
id: 1,
value: ''
};
$scope.save = function(obj) {
question.consoleLog(obj, function() {
console.log('You better have said AngularJS');
});
};
}]);
})();
In general, things related to the UI belong in a directive, things related to the binding of input and output (either from the user or from the server) belong in a controller, and things related to the business/application logic belong in a service (of some variety). I've found this separation leads to very clean code for my part.

Twitter typeahead.js: Possible to use Angular JS as template engine? If not how do I replace "{{}}" for Hogan/Mustache js?

I am working with twitter's typeahead.js and I was wondering if it was possible to modify hogan.js to use something other than {{}}?
I am looking at the minified code now and I have no idea what to change for something so simple. Doing a find and replace breaks it.
I am asking this mainly because I'm using Angular JS but twitter's typeahead requires a templating engine, causing hogan and angular's {{}} to clash. An even better solution would be simply modifying Angular JS (I know it's not a templating engine) and ditching Hogan to fit the following criteria:
Any template engine will work with typeahead.js as long as it adheres to the following API:
// engine has a compile function that returns a compiled template
var compiledTemplate = ENGINE.compile(template);
// compiled template has a render function that returns the rendered template
// render function expects the context to be first argument passed to it
var html = compiledTemplate.render(context);
Ignore the documentation on this, just look at the source code:
function compileTemplate(template, engine, valueKey) {
var renderFn, compiledTemplate;
if (utils.isFunction(template)) {
renderFn = template;
} else if (utils.isString(template)) {
compiledTemplate = engine.compile(template);
renderFn = utils.bind(compiledTemplate.render, compiledTemplate);
} else {
renderFn = function(context) {
return "<p>" + context[valueKey] + "</p>";
};
}
return renderFn;
}
It happens you can just pass a function to template, callable with a context object which contains the data you passed in the datum objects at the time of instantiation, so:
$('#economists').typeahead({
name: 'economists',
local: [{
value: 'mises',
type: 'austrian economist',
name: 'Ludwig von Mises'
}, {
value: 'keynes',
type: 'keynesian economist',
name: 'John Maynard Keynes'
}],
template: function (context) {
return '<div>'+context.name+'<span>'+context.type.toUpperCase()+'</span></div>'
}
})
If you want to use Hogan.js with Angular, you can change the delimiters by doing something like:
var text = "my <%example%> template."
Hogan.compile(text, {delimiters: '<% %>'});
It appears that the template engine result that typeahead.js expects is an html string and not the dom element (in dropdown_view.js). So I am not sure there is a good solution for using an angular template. As a test I was able to get it binding the result to an angular template but it has to render to an element and then get the html value from the element after binding with the data. I don't like this approach but I figured someone might find it useful. I think I will go with a template function like in the previous post.
Jade template looks like
.result
p {{datum.tokens}}
p {{datum.value}}
Directive
angular.module('app').directive('typeahead', [
'$rootScope', '$compile', '$templateCache',
function ($rootScope, $compile, $templateCache) {
// get template from cache or you can load it from the server
var template = $templateCache.get('templates/app/typeahead.html');
var compileFn = $compile(template);
var templateFn = function (datum) {
var newScope = $rootScope.$new();
newScope.datum = datum;
var element = compileFn(newScope);
newScope.$apply();
var html = element.html();
newScope.$destroy();
return html;
};
return {
restrict: 'A',
link: function (scope, element, attrs, ctrl) {
element.typeahead({
name: 'server',
remote: '/api/search?q=%QUERY',
template: templateFn
});
element.on('$destroy', function () {
element.typeahead('destroy');
});
element.on('typeahead:selected', function () {
element.typeahead('setQuery', '');
});
}
};
}
]);

Resources