I have an array of templates in an AngularJS controller which display depending on which array item is selected like so:
function controller() {
vm.templates = [
{ name: 'form1200intro.html', url: require('html-loader!../html/europct/europct.form1200.intro.tpl.htm')},
{ name: 'form1200questions.html', url: require('html-loader!../html/europct/europct.form1200.questionnaire.tpl.htm')},
{ name: 'form1200generated.html', url: require('html-loader!../html/europct/europct.form1200.generated.tpl.htm')}
];
}
$scope.form1200Template = vm.templates[0].url;
And in the html I am displaying them using the ng-bind-html directive, like so:
<div data-ng-bind-html="form1200Template">
</div>
The HTML is loading but none of the expressions are compiling.
Question
How do I display the html with all expressions compiled?
Related
The handlebars.js template is generated server side and returned as part of the response.
"<div>{{name}}<small style='color:#999;'><br><code>{{vafm}}</code></small><br><code>{{email}}</code></div>"
.
var t = "";
$('#UniqueId').select2({
ajax: {
url: '/search/endpoint',
dataType: 'json',
processResults: function (data) {
t = data.template;
return {
results: data.results
};
},
},
templateResult: Handlebars.compile(t),
escapeMarkup: function (m) {
return m;
}
});
Unfortunately the rendered part on select2 does not contain the values returned by the data.results
I have located the issue to this line
templateResult: Handlebars.compile(t),
since trying something like
<script>
const template = Handlebars.compile(#Html.Raw(Model.Template));
</script>
and
templateResult: template,
works as expected.
In the last example i pass the template from the model,
but not i need to pass it from the ajax response and achieve the same output.
There are multiple problems:
templateResult expects a callback function, but you are passing a compiled Handlebars template object. It's described here in the select2 docs. So, assuming that you already did the following in the right place:
var t = Handlebars.compile(...)
Then something like this would work:
templateResult: function(data) {
if (!data.id) return data.text;
return $(t({
name: 'example name',
vafm: 'example vafm',
email: 'example email'
}));
The template html must have one enclosing element, so put things in a <div></div>, for example
Your template is missing a <small> opening tag
So let's assume your server sends some JSON like the following. The template for every result is sent along and can be different for every result. Same goes for the template-specific data:
[
{
"id": 1,
"text": "Henry",
"data": {
"vafm": "1234",
"email": "henry#example.com"
},
"template": "<div>{{text}}<br><small>vafm: {{data.vafm}}</small></div><small>email: {{data.email}}</small>"
}, {
"id": 30,
"text": "Tesla Roadster",
"data": {
"price": "$200.000",
"color": "dark red"
},
"template": "<div>{{text}}<br><small>price: {{data.price}}</small></div><small>color: {{data.color}}</small>"
}
]
Together with something like this in your JavaScript, it should work:
$('#UniqueId').select2({
ajax: {
url: '/search/endpoint',
dataType: 'json',
processResults: function(data) {
/** Note: select2 expects "results" to be an array of objects, each containing
* at least "id" (becomes the value) and "text" (displayed text). I made the
* format from the server match that (plus added some more info), so that I don't
* need any conversions except for compiling the template. I do that with
* Array.prototype.map() in this case, but of course a for-loop would work too.
*/
return {
results: data.map(function(e) {
e["template"] = Handlebars.compile(e["template"]);
return e;
})
};
}
},
templateResult: function(el) {
if (!el.id) return el.text;
/* Note: "el" will be just like it came from the server, except for the
* modifications we applied in processResults. We can just pass "el" to
* the compiled template entirely and there, only pick what we need.
*/
return $(el["template"](el));
}
});
Update 2 added, see below
First of all, this is the starting point of the framework I am working with (and needs to fix):
// index.html
<!doctype html>
<html ng-app="myApp">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="index.js"></script>
<body>
<div ng-controller="outerController">
<div id="copy"></div>
<hr>
<div id="src" ng-controller="innerController">
<table>
<th>Name</th>
<th>Type</th>
<tbody>
<tr ng-repeat="poke in pokemon">
<td>{{ poke.name }}</td>
<td>{{ poke.type }}</td>
</tr>
<tr>
<td>Pikachu</td>
<td>Electric</td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>
// index.js
var app = angular.module("myApp", []);
app.controller("innerController", function($scope) {
$scope.pokemon = [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
});
app.controller("outerController", function($scope) {
$("#copy").html($("#src").html());
});
So as you can see, the child controller will generate a table from its scope's data via ng-repeat. This step is successful. The next step is for the parent controller to copy-paste the inner HTML from src to copy. The intent is for copy to contain a copy of the complete table fully generated by angularJS inside src.
This step has failed. Only the table headers and the static Pikachu row is visible. After doing some research I am certain that this is because pokemon is inside the child controller's scope which is inaccessible by the parent controller. The HTML copied into the copy container includes the entire ng-repeat directive. This copied directive is inside the parent scope, where $scope.pokemon does not exist/contains no data, which is why the ng-repeat in copy generated nothing.
I cannot put the data inside the parent controller. In my actual application, the system uses a modular design. Each inner controller represents a module which pulls its own set of data from the server. There are multiple web pages (represented by the outer controller) which have a many-to-many relationship with the modules, and the composition of modules in each web page needs to be modifiable. That means the data used by a module must be contained within itself.
How can I rectify this?
Update 1: Redacted. I posted an example of using $emit and $on but Robert's example should be assumed as correct, since I'm still very new to this. Refer to his answer.
Update 2: While testing Alvaro Vazquez's & Robert's solutions, I've identified the specific root cause. When $("#copy").html($("#src").html()); is executed, either the copied ng-repeat executed before any data transfer to outerController occurred, or it was never executed. In the end, modifying what I originally did above makes it fully working:
var app = angular.module("myApp", []);
$(function() {
$("#copy").html($("#src").html());
});
app.controller("innerController", function($scope) {
$scope.pokemon = [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
});
app.controller("outerController", function($scope) {
$scope.pokemon = [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
});
With the location of that particular statement changed, all that is left is to transfer the data to outerController, and at this point both Alvaro's and Robert's solutions work.
As an aside, I think some have advised against using $("#copy").html($("#src").html());. As I have partly described in the comments, the actual application I'm developing consists of multiple web pages, each containing its own outerController. Each innerController is in its own separate HTML file added via an include directive into src. The outerController copies the inner HTML of src, passes it to a third party library, which pastes it into copy and controls its visual layout. $("#copy").html($("#src").html()); is actually part of the third party library's implementation, so I can't change that. Using this statement is therefore a requirement.
I'll post the above as a solution when I get home and has the convenience of a PC keyboard. In the meantime feel free to recommend better solutions to what is found if you have one, thanks!
I think you should make use of angular services.
Declaring a service
First of all, you should declare a service which would 'serve' the data to the rest of your application. For the sake of simplicity, I will only show a method which returns a predefined array, but you could get the data from the server here.
app.service('pokemonService', function () {
return {
getPokemon: function () {
return [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
}
};
});
Using the service in your controller
Then, you can use the service on any of your controllers, injecting it as any other predefined angular service:
app.controller('innerController', function($scope, pokemonService) {
$scope.pokemon = pokemonService.getPokemon();
});
app.controller('outerController', function($scope, pokemonService) {
$scope.outerPokemon = pokemonService.getPokemon();
});
Showing the data in your view
Finally, you can list all your pokémon in any template/part of the template you want:
<!doctype html>
<html ng-app="myApp">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="index.js"></script>
<body>
<div ng-controller="outerController">
<div id="copy">
<!-- Here you can also list the pokémon from your outerController, maybe this time in a list -->
<ul>
<li ng-repeat="poke in pokemonOuter">
{{ poke.name }} - <span class="type">{{ poke.type }}</span>
</li>
</ul>
</div>
<hr>
<div id="src" ng-controller="innerController">
<table>
<th>Name</th>
<th>Type</th>
<tbody>
<tr ng-repeat="poke in pokemon">
<td>{{ poke.name }}</td>
<td>{{ poke.type }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>
Wrap up
As you can see, there is no need of messing with the DOM at all. If you use AngularJS, you should do things the Angular way, and working directly with the DOM is not the Angular way at all. Instead, you should put all your data and business logic into services, then use those services in your controllers to retrieve that data and pass it to the view.
Scopes in Angular uses prototypal inheritance, so the child scope will have access to the parent properties but the parent will not have access to the child controller scope properties.
You can use a service to share data or use $emit to send events upwards (upwards until the root scope).
I created a plnkr for you to show you how to use emit (you can find it here)
var app = angular.module("myApp", []);
app.controller("outerController", ['$scope', function($scope) {
console.log('aici');
$scope.$on('pokemonEvent', function(event, mass) { console.log(mass); });
}]);
app.controller("innerController", ['$scope', function($scope) {
$scope.pokemon = [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
$scope.$emit('pokemonEvent', $scope.pokemon);
}]);
I'll outline what I've set up and what I'd like to work:
I have a list of items in my template like this:
<ul>
<li ng-repeat="user in users">
<a ui-sref="user({slug: user.slug, id: user.id})">
{{user.name}}
</a>
</li>
</ul>
I have a state provider looking like this:
.state('user', {
url: '/:slug',
controller: 'UserCtrl',
templateUrl: 'app/user/userProfile.html',
resolve: {
user: function($stateParams, UserService) {
return UserService.get($stateParams.id);
}
}
});
Now I understand that this isn't working because the id parameter is not defined in the url and so it isn't passed to the $stateParams. I have seen similar questions answered with suggestions of using something like this:
resolve: {
user: function($stateParams, UserService) {
var userId = UserService.getIdFromSlug($stateParams.slug);
return UserService.get(userId);
}
}
But the getIdFromSlug method is either going to require another API call or when I get the list, creating a mapping object in the service like { slug: id }. Both don't seem like very elegant solutions, especially since I already have the id.
Am I missing something? Is there no other way to pass parameters through to the state provider? (I am fairly new to Angular).
.state('user', {
url: '/:slug',
params: { id: { value: -1 } },
I have used the above so the ids where not passed in the url.
You can pass both slug and id , but than you have to code both in the url
url: '/:slug/:id'
like this you can pass both
You can add id in the url like
url: '/:slug/:id'
Or treat it as the query string like below
url: '/:slug&id
I'm working with backbone and lazy-loading both templates and data for one of my views. For "simplicity" (in terms of not having to do multiple asynchronous server calls), I'm attempting to load both from the same file persons.php, which returns something like this:
<script type='text/template' id='persons-template'>
<div><%= name %></div>
</script>
<script type='text/javascript'>
var persons: [
{ name: "some dude" },
{ name: "some other dude" }
];
</script>
The view itself is loading this file using an Ajax call, and attempting to parse, like so:
var self = this;
$.get("persons.php").function(data) {
self.template = _.template($(data).html());
self.model = new PersonsCollection;
// problem here: persons is undefined
self.model.reset(persons);
self.render();
});
So, persons is undefined. I know why: the script block is not added to the DOM. I can use $(data)[1] to get an HtmlScriptElement; using $($(data)[1]).text() gives me the Javascript as a string like this:
"var persons: [ { name: "some dude" }, { name: "some other dude" } ];"
How can I access the persons object so that I can pass it to my model?
Take any returned scripts with no type, or type 'text/javascript', and stuff it into the DOM before templating.
$.get("persons.php").function(data) {
$(data)
.find('script[type="text/javascript"], script[type!=""]')
.appendTo('head');
// proceed as before
});
Ok, so I'm trying to get a grip on Mustache.js for rendering views in javascript. I have an API that returns a number of "events", which can be a number of different types. I want to render the events in (very) different ways, based on their type:
data : {
events: [ {title: 'hello', type: 'message'}, {title: 'world', type: 'image'} ] }
Ideally, I could do something like this:
{{#events}}
{{#message}}
<div class="message">{{title}}</div>
{{/message}}
{{#image}}
<span>{{title}}</span>
{{/image}}
{{/events}}
But that would (right?) force me to refactor my data into:
data : {
events: [ {message: {title: 'hello'}}, {image: {title: 'world'}} ] }
Is there a better way of solving this, without refactoring my data? Or should I just bite the bullet?
Mustache is logic-less so there's not much you can do with pure template code other than switching to Handlebars.
Your Mustache-friendly alternative would be to declare a helper and use it to select which template to render. It gets a little convoluted but you can avoid switching away from Mustache if that's something you can't change:
var base_template = '{{#events}}' +
'{{{event_renderer}}}' +
'{{/events}}';
var message_template = '<div>message: {{title}}</div>';
var image_template = '<div>image: {{title}}</div>';
var data = {
events: [ {title: 'hello', type: 'message'}, {title: 'world', type: 'image'} ],
event_renderer: function() {
return Mustache.render('{{> ' + this.type + '}}', this, {message: message_template, image: image_template});
}
}
Mustache.render(base_template, data);
The trick here is that you create a base template that will be the iterator, and pass in event_renderer as a helper. That helper will in turn call Mustache.render again, using partials to render each type of event you can have (that's the {{> partial}} notation).
The only ugly part here is that you need to add this event_renderer member to your JSON data, but other than that, it should all be fine (in Handlebars you could declare it as a helper and there's no need to merge it with your data).