I'm looking for a way to integrate something like ng-repeat with static content. That is, to send static divs and to have them bound to JS array (or rather, to have an array constructed from content and then bound to it).
I realize that I could send static content, then remove and regenerate the dynamic bits. I'd like not to write the same divs twice though.
The goal is not only to cater for search engines and people without js, but to strike a healthy balance between static websites and single page applications.
I'm not sure this is exactly what you meant, but it was interesting enough to try.
Basically what this directive does is create an item for each of its children by collecting the properties that were bound with ng-bind. And after it's done that it leaves just the first child as a template for ng-repeat.
Directive:
var app = angular.module('myApp', []);
app.directive('unrepeat', function($parse) {
return {
compile : function (element, attrs) {
/* get name of array and item from unrepeat-attribute */
var arrays = $parse(attrs.unrepeat)();
angular.forEach(arrays, function(v,i){
this[i] = [];
/* get items from divs */
angular.forEach(element.children(), function(el){
var item = {}
/* find the bound properties, and put text values on item */
$(el).find('[ng-bind^="'+v+'."]').each(function(){
var prop = $(this).attr('ng-bind').split('.');
/* ignoring for the moment complex properties like item.prop.subprop */
item[prop[1]] = $(this).text();
});
this[i].push(item);
});
});
/* remove all children except first */
$(element).children(':gt(0)').remove()
/* add array to scope in postLink, when we have a scope to add it to*/
return function postLink(scope) {
angular.forEach(arrays, function(v,i){
scope[i] = this[i];
});
}
}
};
});
Usage example:
<div ng-app="myApp" >
<div unrepeat="{list:'item'}" >
<div ng-repeat="item in list">
<span ng-bind="item.name">foo</span>
<span ng-bind="item.value">bar</span>
</div>
<div ng-repeat="item in list">
<span ng-bind="item.name">spam</span>
<span ng-bind="item.value">eggs</span>
</div>
<div ng-repeat="item in list">
<span ng-bind="item.name">cookies</span>
<span ng-bind="item.value">milk</span>
</div>
</div>
<button ng-click="list.push({name:'piep', value:'bla'})">Add</button>
</div>
Presumable those repeated divs are created in a loop by PHP or some other backend application, hence why I put ng-repeat in all of them.
http://jsfiddle.net/LvjyZ/
(Note that there is some superfluous use of $(), because I didn't load jQuery and Angular in the right order, and the .find on angular's jqLite lacks some features.)
You really have only one choice for this:
Render differently for search engines on the server, using something like the approach described here
The problem is you would need to basically rewrite all the directives to support loading their data from DOM, and then loading their templates somehow without having them show up in the DOM as well.
As an alternative, you could investigate using React instead of Angular, which (at least according to their website) could be used to render things directly on the web server without using a heavy setup like phantomjs.
Related
I am using an element directive on several views. The directive repeats for each 'resource' in a list of resources, simply like this: ng-repeat="resource in resources".
Different page controllers will determine which resources will get pulled from the API by having a different $scope.resourceApiPath in each view controller that the directive controller then uses to make an $http call.
This works well for different views that display different resources. For example one view displays all the user's own resources, one view displays public resources, one view displays resources the user has starred, etc.
The only problem is that for one of my views I want to add a filter on the ng-repeat like so: ng-repeat="resource in resources | filter:resourceSearch.text". is there a way for me to add this filter depending on a condition, e.g. if $scope.filter == true;, otherwise simply returning the regular ng-repeat="resource in resources"?
This is my directive:
.directive('resourcePanel', function() {
return {
restrict: 'E',
scope: false,
templateUrl: 'scripts/templates/resource-panel.html',
controller: 'ResourcesPanelController'
}
})
The ResourcesPanelController referenced in the directive has something like this:
$scope.resources = [];
$scope.loadResources = function() {
$scope.resources = []; // Empty the array
$http.get('/api/resource' + $scope.resourceApiPath)
.then(function(res) {
$scope.resources = res.data.message;
});
};
$scope.loadResources();
The scripts/templates/resource-panel.html element template looks something like this:
<div class="panel" ng-repeat="resource in resources">
{{resource.content}}
</div>
Thanks!
i think you can do one of four ways , i prefer 1 and 3 but its totally depend on your requirement and code standard which one suits best for you.
Custom filter is also a clean solution.
If you are using text value for filter then, if you don't set the value of resourceSearch.text then its as good as no filter, so if resourceSearch.text exist it will filter and if its undefined then no filter.
ng-repeat="resource in resources | filter:resourceSearch.text"
Create a custom filter and handle it there.
if you want to create a custom filter then use enable argument which you can use to on and off your filter http://plnkr.co/edit/ilPJPUZkBC7Y5OvGHWey?p=preview i have found a plunkr as well for custom filter which suits your need
ng-repeat="resource in resources | customfilter:resourceSearch.text:enable"
Filter your final resources in controller and pass filtered value to html
use ng-if with your condition if filter exist then one loop and if its not there then different loop.
i hope it will help.
You could use a custom filter function:
$scope.conditionalFilter = function(resource) {
if (!$scope.filter) {
return true;
}
// normal filter logic goes here, e.g.
return resource.content.indexOf($scope.resourceSearch.text) >= 0;
};
Then in your template just use the custom filter:
<div class="panel" ng-repeat="resource in resources | filter:conditionalFilter">
{{resource.content}}
</div>
<div ng-if="!filter" class="panel" ng-repeat="resource in resources">
{{resource.content}}
</div>
<div ng-if="filter" class="panel" ng-repeat="resource in resources| filter:resourceSearch.text">
{{resource.content}}
</div>
Sorry for the no description, I use this sometimes if im pretty sure it only shows up once, since when you flatten it..
<div ng-if="!filter" class="panel" ng-repeat="resource in resources">{{resource.content}}</div>
<div ng-if="filter" class="panel" ng-repeat="resource in resources|filter:resourceSearch.text">{{resource.content}}</div>
then it really doesn't look that bad.
But a more complete solution would be setting up a a custom filter and injecting 'filter'
app.filter('conditionalFilter', function (filter) {
return function (item, condition) {
if(condition){
return filter.bind(this,item).apply(this,[].slice.call(arguments,2));
} else{ return item }
};
});
and then
<div class="panel" ng-repeat="resource in resources| conditionalfilter : !!filter : researchSearch.text" >
{{resource.content}}
</div>
Haven't tested, let me know if I made a mistake.
I am using the tile example from polymers neon elements - and I am trying to make each expanded tile unique. My first try on how to do this was to pass a string in with the grid items like
{
value: 1,
color: 'blue',
template: 'slide-1'
}
And have that element be evaluated when rendered in a new element something like this. (this is the card template itself)
<template>
<div id="fixed" class$="[[_computeFixedBackgroundClass(color)]]"></div>
<div id="card" class$="[[_computeCardClass(color)]]">
<[[item.template]]></[[item.template]]>
</div>
This does not work - however I am wondering if there is some way to do this so I can load custom elements for the content of each card. For reference -https://elements.polymer-project.org/elements/neon-animation?view=demo:demo/index.html&active=neon-animated-pages , it is the grid example and I am trying to replace the content of each card once it is clicked on ( the fullsize-page-with-card.html, here is all the html for it - https://github.com/PolymerElements/neon-animation/tree/master/demo/grid ). Is this the wrong way of approaching this? Or maybe I have some syntax wrong here. Thanks!
Edit : OK, So I can send it through if i add it to the click to open the card like so
scope._onTileClick = function(event) {
this.$['fullsize-card'].color = event.detail.data.color;
this.$['fullsize-card'].template = event.detail.data.template;
this.$.pages.selected = 1;
};
and in the card's properties like so
template: {
type: String
},
So I can then evaluate it as [[template]] , however - the question still remains how to call a custom element (dynamically) using this string. I could pass a couple of properties and fill in a card or form so they are unique, but i think I would have much more creative freedom if I could call custom elements inside each card.
I have an element that allows referenced templates. There are a couple of others other there, but this one also allows data bindings to work: https://github.com/Trakkasure/dom-bindref
EDIT: Here is the solution sample based on the first answer. You're still welcome to offer further suggestions and fill some gaps in my knowledge (see comments).
plnkr.co/edit/mqGCaBHptlbafR0Ue17j?p=preview
ORIGINAL POST
Hello all Angular heads,
I’m trying to build a tool for building simple web pages from a set of predefined elements. The end user designs the layout by choosing which elements appear in which order (there would be a selection of maybe 20-30 different elements). An example layout:
Heading
Paragraph
Paragraph
Subheading
Barchart
Paragraph
…and so forth
Under the hood, there would be an array of Javascript objects:
var page_structure = [
{type: ”heading”, content: ”The final exam - details” },
{type: ”paragraph”, content: ”Bla bla bla bla bla…” },
{type: ”paragraph”, content: ”Bla bla bla bla bla…” }
…
{type: ”bar chart”, y: ”Grades”, colour: ”blue” }
…
];
The end user inputs parameters like content, variable, colour, etc, for each individual element.
THE PROBLEM
I can’t figure how to draw these elements on the page so that the dynamic parameters for each element are properly included.
Each element type (heading, paragraph, bar chart, other possible elements) has it’s own HTML template, which has to be able to dynamically display user-defined parameters. I’m thinking I need something like this:
var templates = {
heading: ”<h1>USER_CHOSEN_PARAMETER_HERE</h1>”,
paragraph: ”<p>USER_CHOSEN_PARAMETER_HERE</p>”
…
};
I’m using Angular’s ng-repeat directive to draw each element. I have tried using either ng-html-bind…
<div ng-repeat="x in page_structure track by $index">
<div ng-bind-html="templates[x.type]"></div>
</div>
…or a custom directive, which I call mycomponent:
<div ng-repeat="x in page_structure track by $index">
<div mycomponent component='x'></div>
</div>
Both methods work just dandy when there are no user defined parameters in templates. I can’t, however, wire up the parameters. With ng-bind-html, I have tried using an expression markup like this:
var templates = {
heading: ”<h1>{{x.content}}</h1>”,
paragraph: ”<p>{{x.content}}</p>”,
…
};
(I actually define templates in app controller constructor, so it’s $scope.templates = { bla bla } to be precise.)
This is just showing curlies and ”x.content” in the actual web page. How do I refer to a dynamically variable parameter inside ng-html-bind template, or is it even possible?
I also tried the custom directive route, defining the directive as
.directive('mycomponent', function() {
return {
scope: { component: "=" },
template: templates[component.type]
}; )
This was even more messed up, since I actually couldn’t figure out how I should even try to refer to a dynamic parameter here inside the template. So I apologise my inability to offer a meaningful example of what I tried to do.
Any help or working examples of the methods I should use here are greatly appreciated. I’m happy to provide more details if needed (this is my first post in SO, so I’m still trying to get the jist of how to flesh out the questions).
I suggest using ngInclude, in which you can have a funciton that check for the proper template to load based on the passed page structure.
<div ng-repeat="structure in page_structure">
<div ng-include="getMyTemplate(structure)"></div>
</div>
$scope.getMyTemplate = function(structure) {
switch (structure.type) {
case 'heading':
return '/Views/Heading.html' // or id of a pre-loaded template
case 'paragraph':
return '/Views/Paragraph.html' // or id of a pre-loaded template
}
}
I've try use jsrender/jsviews first time. It looks awesome, but I'm not find clear documentation or example how to dynamically bind event handlers for generated content.
For example pure jQuery old approach was:
Code from bean class to render collection of objects:
container = $('#tabs-my');
this.load( // Obtain array of objects
$.proxy(function(list){
container.html('');
list.forEach(
$.proxy(function(it, i){
container.append(this.renderItem(it));
}
,this)
);
}
,this)
);
And in object itself render method:
,renderItem: function(/*Specialist*/ it){
var container = $('<div class="specialist-item" />')
container.append(
$('<span class="x">Remove</span>').click($.proxy(function(){
this.removeSpecialistFromList(it.id)
}
,this))
);
container.append(
$('<span class="x">Edit</span>').click($.proxy(function(){
this.renderSaveForm(container)
}
,this))
);
container.append('<p><b>' + it.name + '</b> <i>' + it.phone + '</i></p>' +
'<p>' + it.skill + '</p>' +
( it.city ? '<p>' + it.city.name + '</p>' : ''));
return container;
}
Note I bind handler via closure content for current object without use any external identificators in tag itself.
Then I try use templating to separate content from visialisation.
My template:
<div class="specialists-list">
Items in list: {{:specialists.length}}
{^{for specialists}}
<div class="specialist-item">
<span class="x">Remove</span><span class="x">Edit</span>
<p><b class="name">{{:name}}</b> <i class="phone">{{:phone}}</i></p>
<p class="skill">{{:skill}}</p>
<p class="city">{{:city.name}}</p>
</div>
{{/for}}
</div>
And rendered like:
var template = $.templates({
specialistsTmpl: tmpl
});
$.templates.specialistsTmpl.link(container, {
specialists: list
});
I realize it could be done using common handlers in attributes something like:
<span class="x" data-id="{{:id}}">Edit</span>
And then try obtain that object again from external. But it is workaround and is not desired.
Is there way to bind handlers in template or via helpers, custom tags?
There are many samples on http://www.jsviews.com which include event binding, and handlers such as for removing or inserting data items. Did you look at the examples here: http://www.jsviews.com/#samples/editable - you will find four different approaches to the same scenario.
For example:
Template
{^{for languages}}
<input data-link="name" />
<img class="removeLanguage" .../>
{{/for}}
Code:
$.link.movieTmpl("#movieList", app)
.on("click", ".removeLanguage", function() {
var view = $.view(this);
$.observable(view.parent.data).remove(view.index);
return false;
});
Note the use of var view $.view(this); - you pass in the element (this) that is clicked on, and $.view(clickedElement) returns you the view, from which you can get view.index (the item index - in the case of iteration over an array), view.data (the current data item - in you case that would be the specialist item), view.parent.data (in your case, the specialists array) etc.
Of course since view.data is the current data item, if your data item is in effect a view model, with methods, you can call a method: view.data.someMethod(...).
But as an alternative to using the jQuery on() for binding handlers, you can use declarative binding directly in the template like this:
{^{for specialists}}
<div class="specialist-item">
<span class="x" data-link="{on removeMe}">Remove</span> ...
...
</div>
{{/for}}
where I assume your specialist has a removeMe() method. The "{on ...}" binding binds by default to the click event, and you can bind to methods on your data, or to helpers, etc.
Take a look at this example: http://jsfiddle.net/BorisMoore/cGZZP/ - which uses {on ...} for binding to helpers for modifying a two-dimensional array.
I hope to create some new samples using {on ...} binding before too long.
BTW I don't recommend using the onAfterCreate for doing event binding. Either of the above approaches are better, and will ensure correctly disposal of event bindings.
I'm not 100% sure I completely understand but I believe I do. You want to use a different approach. Look at .on for jQuery. I've switched to using it most all the time. The call back function doesn't need to change. It is really pretty nice.
In my case, I was creating thousands of event handlers and it was killing my performance. So I switched to using .on and it solved my problem.
This doesn't exactly answer your question... but I think its a better solution.
I am very new to angular js. I want to create an input box on click of particular div. Here I need to create element on div which repeating.
<div><div ng-repeat ng-click="create();"></div><div>
What will be the best way to do so?
DOM manipulation in Angular is done via directives (There is paragraph on 'Creating a Directive that Manipulates the DOM' here)
First, read through this excellent article: How do i think in Angular if i have a jQuery background
The Angular Team also provides a pretty neat tutorial, which definetly is worth a look: http://docs.angularjs.org/tutorial
While Angular is pretty easy and fun to use once you have wrapped your head around the concepts, it can be quite overwhelming to dive into the cold. Start slow and do not try to use each and every feature from the beginning. Read a lot.
I strongly recommend egghead.io as a learning resource. The video-tutorials there are bite-sized and easy to watch and understand. A great place for both beginners and intermediates. Start from the bottom here.
Some folks have done great things with Angular. Take a look at http://builtwith.angularjs.org/ and check out some source code.
Use an array and ng-repeat to do that. Have a look at the following code.
I crated scope variable as an empty array. Then created a function to add values to that array.
app.controller('MainCtrl', function($scope) {
$scope.inputFields = [];
$scope.count = 0;
$scope.addField = function(){
$scope.inputFields.push({name:"inputText"+$scope.count++});
}
});
I used ng-repeat with this array. and called the function on the click event of a div.
<div ng-click="addField()">Click here to add</div>
<div ng-repeat="inputField in inputFields">
<input type="text" name="inputField.name">
</div>
Check this working link
Update - Show only one text box on click
I created addField() as follows.
$scope.addField = function(){
$scope.newTextField = "<input type='text' name='myTxt'>";
}
To render this html in my view file I created a new directive called compile as follows.
app.directive('compile', function($compile) {
// directive factory creates a link function
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
function(value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
}
);
};
});
Then used this directive in my view.html file
<body ng-controller="MainCtrl">
<div ng-click="addField()">Click to Add</div>
<div compile="newTextField"></div>
</body>
click here to view the working link