I'm building a "tour guide" for my angular powered website.
I looked around for options and Intro.js seemed like the best fit. It had an Angular Directive already ready and everything: Angular Intro.js.
Everything worked as expected, until I had to add a step to first (and only the first) a DOM object that is being injected by a ng-repeat binding. I gave all ng-repeat items a unique ID (via $index) but Intro.js just fails to acknowledge it.
I'm guessing Intro is trying to find the DIV by the ID specified, but since the ng-repeat isn't complete yet, there's no DIV by that name.
I made a plunker where you can see that it' working on static content but fails to aknowledge the objects inside the ng-repeat.
Relevant code:
HTML:
<!-- Works -->
<div id="static">This is static content.</div>
<!-- Doesnt work -->
<div id="item{{$index}}" ng-repeat="item in items">
{{ item.name }}
</div>
Angular Controller:
$scope.IntroOptions = {
steps:[
{
element: document.querySelector('#static'),
intro: "This is static content"
},
{
/** ID "item0" belongs to the first element on the ng-repeat **/
element: document.querySelector('#item0'),
intro: "Doesnt work!"
}
],
showStepNumbers: false,
exitOnOverlayClick: true,
exitOnEsc:true,
nextLabel: '<strong>NEXT!</strong>',
prevLabel: '<span style="color:green">Previous</span>',
skipLabel: 'Exit',
doneLabel: 'Thanks'
};
Plunker: http://plnkr.co/edit/kE8P5Kq2Y5CVWEYgyBIo?p=preview
Assuming the reason above is the reason this isn't working, how do I
make the directive wait for the DOM to be "ready"?
If my assumption is wrong, why isn't it working then?
.directive('onLastRepeat', function() {
return function(scope, element, attrs) {
if (scope.$last) setTimeout(function(){
scope.$emit('onRepeatLast', element, attrs);
}, 50);
};});
Use this directive like this :
<div id="item{{$index}}" ng-repeat="item in items" on-last-repeat>
{{ item.name }}
</div>
in your controller wait for finish the loop and call introjs
$scope.$on('onRepeatLast', function (eve, elem) {
//call intro
});
The most clueless approach I'd attempt at first would be to wrap the dynamic content:
<div id="dynamic">
<div id="item{{$index}}" ng-repeat="item in items">
{{ item.name }}
</div>
</div>
The scope variable then becomes
$scope.IntroOptions = {
steps:[
{
element: document.querySelector('#static'),
intro: "This is static content"
},
{
element: document.querySelector('#dynamic'),
intro: "This is dynamic content"
}
],
// ... cut ...
It looks like your original code is deadlocking: the view depends on the data ($scope.items) which depends on the view (IntroOptions)
You can observe for the $viewContentLoaded event that $rootScope will broadcast.
I know this is quite old, but if anyone stumbles upon this answer, the best thing to do is to use a getter, it gets evaluated the moment it needs to access the property:
$scope.IntroOptions = {
steps:[
{
element: document.querySelector('#static'),
intro: "This is static content"
},
{
/** ID "item0" belongs to the first element on the ng-repeat **/
get element(){return document.querySelector('#item0')},
intro: "Doesnt work!"
}
],
showStepNumbers: false,
exitOnOverlayClick: true,
exitOnEsc:true,
nextLabel: '<strong>NEXT!</strong>',
prevLabel: '<span style="color:green">Previous</span>',
skipLabel: 'Exit',
doneLabel: 'Thanks'
};
Related
I have divs, which generates dynamically, based on server data. All this divs have their keyboard values: numpad or fullkeyboard (based on server data).
For example:
[
{
label: 'NICK NAME',
keyboard: 'fullKeyboard'
},
{
label: 'AGE',
keyboard: 'numpad'
}
]
And I have templates, for both keyboard.
I want that, when user clicks on those divs, display correct key-board template.
How it is possible to do?
MY HTML:
<div id="container-of-inputDivs">
<div ng-repeat="(key, item) in dataSource" data-keyboard="item.keyboard"></div>
</div>
<keyboard />
Your keyboard directive must have some scope attribute, like so:
<div keyboard="activeKeyboard"></div>
Then, your divs could look like this (assuming that dataSource is the array you provided in the question):
<div id="container-of-inputDivs">
<div ng-repeat="item in dataSource" ng-click="activeKeyboard=item">
</div>
This way, when you click on the div, the activeKeyboard gets set to item, which is stored inside the variable activeKeyboard on the scope.
From your directive, you can return something like this:
{
template: 'your-template-here',
scope: {
keyboard: '='
},
...
}
You isolate the scope with two-way-binding . You could now use the isolated keyboard object inside your template.
EDIT:
EXAMPLES ON HOW TO USE IT
There are many ways to go from here. Here are two examples:
1) With dynamic code inside template
{
template: '<div>{{keyboard.label}}</div>',
scope: {
keyboard: '='
},
...
}
2) Different templates based on scope
Suppose you have two different templates you want to include, based on your keyboard's scope: /templates/fullKeyboard.html and /templates/numpad.html.
{
template: '<div ng-include="getTemplateUrl()"></div>',
scope: {
keyboard: '='
},
controller: function($scope) {
$scope.getTemplateUrl = function() {
return '/templates/' + $scope.keyboard.keyboard + '.html';
}
}
}
As I said, there are many ways of doing it, depends on your architecture and what you want to achieve.
You could use ng-include and then have a function returning the correct html template depending on the keyboard variable.
ex:
vm.GetKeyboardTemplate = function(keyboard){return keyboard + ".html";}
<div ng-include="{{vm.GetKeyboardTemplate(item.keyboard);}}"></div>
I have broken this problem down into it's simplest form. Basically I have a directive that, for the demo, doesn't yet really do anything. I have a div with the directive as an attribute. The values within the div, which come from an object array, are not displayed. If I remove the directive from the div, they are displayed OK. I am clearly missing something really obvious here as I have done this before without any problems.
Here's the Plunk: http://plnkr.co/edit/ZUXD4qW5hXvB7y9RG6sB?p=preview
Script:
app.controller('MainCtrl', function($scope) {
$scope.tooltips = [{"id":1,"warn":true},{"id":2,"warn":false},{"id":3,"warn":true},{"id":4,"warn":true}];
});
app.directive("cmTooltip", function () {
return {
scope: {
cmTooltip: "="
}
};
});
HTML
<div ng-repeat="tip in tooltips" class="titlecell" cm-tooltip="true">
A div element: {{ tip.id }}
</div>
<br><br>
Just to prove it works without the directive:
<div ng-repeat="tip in tooltips" class="titlecell">
A div element: {{ tip.id }}
</div>
There is a hack to make it working in earlier versions of angular by making use of transclusion, like that:
app.directive("cmTooltip", function () {
return {
scope: {
cmTooltip: "="
},
transclude: true,
template : '<div ng-transclude></div>'
};
});
PLNKR
As by Beyers' comment above and below, the behaviour the question is about no longer exists in at least 1.2.5
To be clearer; this has nothing to do with ng-repeat, you can remove it and there still will be no tip ( or tooltips ).
See this question on what the = and other configs mean and what it is doing for you.
Basically for your situation when you use = the scope of the directive will be used in the underlying elements, you no longer have your controller's scope. What this means for you is that there is no {{ tip.id }} or not even tip. Because the directive doesn't supply one.
Here's a plunker that demonstrates what you can do with it.
Basically all i did was
app.directive("cmTooltip", function () {
return {
scope: {
cmTooltip: "="
},
link: function($scope){ // <<
$scope.tip = { id: 1 }; // <<
} // <<
};
});
This creates the tip object on the scope so it has an id.
For your situation you would probably just not use = and look at this question for your other options depending on what you want.
In my opinion this isn't the way to go.
I would use Objects.
JS code:
function tooltip(id,warn){
this.id = id;
this.warn = warn;
}
tooltip.prototype.toString = function toolToString(){
return "I'm a tooltip, my id = "+this.id+" and my warn value = "+this.warn;
}
$scope.tooltips = [new tooltip(1,true),new tooltip(2,false),new tooltip(3,true),new tooltip(4,true)];
HTML:
<div ng-repeat="tip in tooltips" class="titlecell">
A div element: {{ tip.toString() }}
</div>
I'm using angular js bootstrap tooltip to show tooltips on a set of elements.
Plunker: http://plnkr.co/edit/9xk41f3CR0wnajN71bSi
I need to inject into the tooltip html compiled by angular, but i don't really get how. The tooltip tutorial is not useful to me because it gets the html from the scope as variable, but for a set of elements this is not possible.
How can i fill tooltip-html-unsafe?
You can do something like this:
HTML:
<li ng-repeat="phone in phones">
<div phone-info index="{{$index}}">
<p tooltip-html-unsafe="{{tooltips[$index] }}">A tooltip should appear on top of this line ({{ phone.name }} - {{ phone.snippet }})</p>
<div>
</li>
Add to controller:
$scope.tooltips = [];
Directive:
app.directive('phoneInfo', function($compile, $timeout) {
/* wrap in root element so we can get final innerHTML*/
var tipTemplate = '<div><p> This will be the content of {{phone.name}} injected in the tooltip </p><div>';
return {
link: function(scope, el, attrs) {
var tipComp = $compile(tipTemplate)(scope)
$timeout(function() {
scope.tooltips[attrs.index] = tipComp.html()
});
}
}
})
Used index to avoid creating an isolated scope. Can also be done with isolated scope and create a property of phone instead of using scope.tooltips[index]
DEMO
I am using "draggable" directive to support image dragging. However, as per the role of the user, I need to disable image dragging for certain groups of users. I have used following code.
<!--draggable attribute is used as handle to make it draggable using jquery event-->
<li ng-repeat="template in templates" draggable id="{{template._id}}" type="template" class="template-box">
<!-- Images and other fields are child of "li" tag which can be dragged.-->
</li>
The method dragSupported is in the template scope and returns true or false. I don't want to create two big duplicate <li> elements by using ng-if for each value returned by dragSupported(). In other words, I am not looking for the following approach to solve this.
<!--draggable attribute is used as handle to make it draggable using jquery event-->
<li ng-if="dragSupported() ==true" ng-repeat="template in templates" draggable id="{{template._id}}" type="template" class="template-box">
<!-- Images and other fields are child of "li" tag which can be dragged.-->
</li>
<!--remove "draggable" directive as user doesn't have permission to drag file -->
<li ng-if="dragSupported() !=true" ng-repeat="template in templates" id="{{template._id}}" type="template" class="template-box">
<!-- Images and other fields are child of "li" tag which can be dragged.-->
</li>
Is there any other approach to avoid code duplicity?
ng-attr-<attrName>
Support for conditionally declaring an HTML attribute is included with Angular as the dynamically-titled ng-attr-<attrName> directive.
Official Docs for ng-attr
Example
In your case, the code might look like this:
<li
id="{{template._id}}"
class="template-box"
type="template"
ng-repeat="template in templates"
ng-attr-draggable="dragSupported() === true"
></li>
Demo
JSFiddle
This contains examples of usage for the following values: true, false, undefined, null, 1, 0, and "". Note how typically-falsey values may yield unexpected results.
Thanks Jason for your suggestion. I took little different approach here. Since I don't want to change the "scope" variable therefore I used "attrs" to check if drag is allowed or not. Following is approach I tool which seems good so far.
Directive code:
app.directive('draggable', function () {
return {
// A = attribute, E = Element, C = Class and M = HTML Comment
restrict: 'A',
replace:true,
link: function (scope, element, attrs) {
if(attrs.allowdrag =="true")
{
element.draggable({
cursor: 'move',
helper: 'clone',
class:'drag-file'
});
}
}
}
});
HTML Code:
<ul>
<!--draggable attribute is used as handle to make it draggable using jquery event-->
<li ng-repeat="template in templates" draggable allowdrag="{{userHasPrivilege()}}" >
<!--Ohter code part of li tag-->
</li>
</ul>
Controller is having implementation of userHasPrivilege().
Not sure if this is correct way or not. Looking for thoughts.
There is no way to directly add or remove an attribute from an element. However, you could create a directive that simply adds the attribute to the element when the condition is met. I've put something together that illustrates the approach.
Demo: http://jsfiddle.net/VQfcP/31/
Directive
myApp.directive('myDirective', function () {
return {
restrict: 'A',
scope: {
canDrag: '&'
},
link: function (scope, el, attrs, controller) {
/*
$parent.$index is ugly, and it's due to the fact that the ng-repeat is being evaluated
first, and then the directive is being applied to the result of the current iteration
of the repeater. You may be able to clean this by transcluding the repeat into the
directive, but that may be an inappropriate separation of concerns.
You will need to figure out the best way to handle this, if you want to use this approach.
*/
if (scope.canDrag&& scope.canDrag({idx: scope.$parent.$index})) {
angular.element(el).attr("draggable", "draggable");
}
}
};
});
HTML
<ul>
<!-- same deal with $parent -->
<li ng-repeat="x in [1, 2, 3, 4, 5]" my-directive="true" can-drag="checkPermissions(idx)">{{$parent.x}}</li>
</ul>
Controller
function Ctl($scope) {
$scope.checkPermissions = function(idx) {
// do whatever you need to check permissions
// return true to add the attribute
}
}
I used a different approach as the previous examples didn't work for me. Perhaps it has to do with using custom directives? Perhaps someone can clear that up.
In my particular example, I'm using ui-grid, but not all ui-grids should use pagination. I pass in a "paginated" attribute and then $compile the directive based on true/false. Seems pretty brutish but hopefully it can push people in a positive direction.
HTML
<sync-grid service="demand" paginated="true"></sync-grid>
Directive
angular
.module('app.directives')
.directive('syncGrid', ['$compile', SyncGrid]);
function SyncGrid($compile){
var nonPaginatedTemplate = '' +
'<div>' +
' <div ui-grid="gridOptions" class="grid"></div>' +
'</div>';
var paginatedTemplate = '' +
'<div>' +
' <div ui-grid="gridOptions" class="grid" ui-grid-pagination></div>' +
'</div>';
return {
link: link,
restrict: 'E',
replace: true
};
function link(scope, element, attrs) {
var isPaginated = attrs['paginated'];
var template = isPaginated ? paginatedTemplate : nonPaginatedTemplate;
var linkFn = $compile(template);
var content = linkFn(scope);
element.append(content);
// Continue with ui-grid initialization code
// ...
}
}
So, I'm new to AngularJS and I'm trying to change a div content after another is clicked (this one holds a div with the content that I want to put on the first one).
HTML
<div ng-controller="dCtrl">
<ul ng-repeat="product in products">
<li change>
{{product.name}}
<div class="hide">{{product.description}}</div>
</li>
</ul>
</div>
<div id="test"></div>
Javascript
var app = angular.module("dt", []);
app.directive("change", function() {
return function(scope, element) {
element.bind("click", function() {
var message = element.children("div").text();
console.log("breakpoint");
angular.bind("#test", function() {
this.text(message);
});
})
}
})
app.controller("dCtrl", function($scope) {
$scope.products = [
{ "name" : "Escova XPTO", "description": "Lava tudo num instante"},
{ "name" : "Pasta de Dentes YMZ", "description": "Dentifrico do camandro"}
];
})
I know that I could just say:
$("#test").html(message);
But I'm still confused about mixing jQuery and AngularJS, I dont know if that is a correct way of doing it
Thanks
Setup ng-click:
ngClick is for doing things such as the scary jQuery-esque stuff you have going on in your change directive. Place ng-click in your clickable div's attributes and pass in a method that changes the $scope variable accepted by...
ngShow and ngHide.
When true these directives, as the name states, show or hide the associated html object. You can pass in $scope variables determine the boolean value. When the $scope updates these methods automatically update the DOM to show/hide the element.