I'm building an app in AngularJS where the user creates some app state, then there are a number of different ways to render it. (For a lame example, let's say they're going to enter three numbers, and then they can choose to render them into a line chart or a pie chart.)
Each rendering type lives in a different HTML file, and I'm letting the user choose a renderer with a <select> which drives the src for an ng-include to load the different renderers. This works fine, so far.
Now, some rendering modes have additional controls; for example, the pie chart might have a "3D" checkbox. Different renderers will have entirely distinct controls. So I want these included files to also create controls for customizing their presentation.
Here's my question: how do I load a single file and let it create its additional control in one spot in the DOM while putting its main content into another spot in the DOM? The additional control cannot appear adjacent to the rendered content in the DOM: it goes in an entirely different location.
Here is an example: http://plnkr.co/edit/1RXVVu?p=preview. I would like, in a.html and b.html, to be able to instantiate their popup controls in the DOM above the <hr>, while having their textual content below the <hr>. (While it would be possible in the example to just put an hr tag in each of a.html and b.html, the real DOM structure is much more complex and doesn't afford that.)
Thanks!
I created a directive to move the element to another container. Here is what I changed from your plunker.
I added jQuery and the directive:
<script type="text/javascript">
(function() {
var app = angular.module('myapp', []);
app.directive('myContainer', [function() {
return {
restrict:'A',
link: function(scope, elem, attrs) {
angular.element(attrs.myContainer).replaceWith(elem);
//angular.element(attrs.myContainer).html(elem);
//angular.element(attrs.myContainer).appendTo(elem);
//etc...
}
};
}]);
})();
</script>
I added a container above the hr:
<div id="#container"></div>
I specified the directive attribute with the container id in a.html and b.html:
<select ng-model="color" ng-options="c for c in ['red', 'blue']" data-my-container="#container"></select>
The container id (or selector) could come from the model dynamically.
I worked the Plunker into a complete solution here: http://tdierks.github.io/angular-element-mover/, code is here: https://github.com/tdierks/angular-element-mover.
The critical directive is:
(function() {
var app = angular.module('controlMover', []);
app.directive('moveTo', [function() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
elem.appendTo(angular.element(attrs.moveTo));
scope.$on("$destroy", function() {
elem.remove();
});
}
};
}]);
})();
One note: I had to listen for $destroy events on the scope to remove the moved DOM element when the pane changes. I haven't yet checked to be sure there's no leakage here, but it's a concern.
Random other things I ran into, for the education of others:
JQuery needs to be loaded before Angular.
This app extension with the directive needs to be named on the ng-app directive in the HTML.
Related
I'm making a directive that resizes a div based on changes in the controller. I need to calculate the amount of available space left in the browser window when changes happen to the model. How do you pass in the element from the link function into the $watch function?
In short, how do I manipulate the DOM based on changes to the model?
var module = angular.module('cmsApp')
module.directive("changeWidth", function($timeout) {
return {
restrict: 'A',
link: function($scope, element, attrs) {
width = element.width();
$scope.$watch('currentFolder', function(value){
// manipulate dom here
});
}
}
});
<!-- need to calculate the size of this -->
<div change-width class="col-md-9 right-pannel"></div>
I don't think Angular is even executing your directive based on your template code. It should be
<div change-width class="col-md-9 right-pannel"></div>
I know this is a source of errors if you are new to Angular. From the docs:
Angular uses name-with-dashes for its custom attributes and camelCase
for the corresponding directives which implement them)
I have created a directive that check if data was entered to an HTML element in the following way:
var myApp = angular.module('myApp', []);
myApp.directive("uiRequired", function () {
return function (scope, elem, attrs) {
elem.bind("blur", function () {
var $errorElm = $('#error_testReq');
$errorElm.empty();
if (angular.isDefined(attrs) && angular.isDefined(attrs.uiRequired) && attrs.uiRequired == "true" && elem.val() == "") {
$errorElm.append("<li>Field value is required.</li>");
$errorElm.toggleClass('nfx-hide', false);
$errorElm.toggleClass('nfx-block', true);
}
else
{
$errorElm.toggleClass('nfx-hide', true);
$errorElm.toggleClass('nfx-block', false);
}
});
};
});
A working example can be seen here
My question:
Is there a way of adding the directive (uiRequired) I have created dynamically to elements on screen on document ready.
I want to put the new directive on selected HTML elements according to pre-defined list I have. I can not know in advance on which field this directive has to be on.
So I have to put it while page is rendering.
I have tried putting it dynamically myself while page is loading, however AngularJS did interpret it.
I could not find an example on the internet that does that.
Can anyone help me?
You can dynamically add directives to a page during the compilation phase when Angular is walking the DOM and matching elements to directives. Each step of the compilation process may transform the DOM tree ahead of it, but you should never modify elements that Angular has already compiled. This point is important to remember because adding directives to elements that have already been walked will have no effect. Of course, there ways around this. For example, you could re-compile and re-link previously walked elements. However I strongly advise against this as it may lead to unintended side effects such as memory leaks, and slow performance.
To dynamically add uiRequired directives, you can create a parent directive - let's call it uiRequiredApplier.
app.directive('uiRequiredApplier', function($scope) {
return {
restrict: 'A',
compile: function(element, attr) {
// you can apply complex logic figure out which elements
// you want to add the uiRequired attribute to
$('input', element).attr('uiRequired','');
return function(scope, element, attr) {
}
}
}
});
You can then apply the attribute like this:
<div ui-required-applier>
<input type="text" ng-model="name" />
</div>
When the uiRequiredApplier is compiled, it will dynamically add uiRequired attributes to selected elements using jQuery that have not been compiled yet. And when Angular walks the DOM, eventually it will compile and link the uiRequired attributes, which will add the desired validation behavior.
I'm using inline editing with CKEditor, and I'd like to bind an element to an angular scope value.
<div contentEditable="true">
<p>Here is the value: {{testval}}</p>
</div>
testval should update in the same manner as it would outside the editor.
To protect this text in the editor, I'd like to do something similar to the placeholder plugin. In other words I plan to have a placeholder, dynamically displaying the final text rather than just the placeholder.
I've seen several examples of how to bind the entire contents with angular, but not individual elements. I'm still fairly new to both angular and ckeditor, so any help or pointers would be much appreciated.
It sounds to me like you will need to use a directive for what you want. I might be soewhat off because I'm not completely familiar, but goig by what you've provided, let's assume this example.
html
<body ng-app="myApp">
<div content-editable content="content"></div>
</body>
javascript
angular.module('myApp', [])
.directive('contentEditable', function() {
restrict: 'A',
replace: true,
scope: {
// Assume this will be html content being bound by the controller
// In the controller you would have:
// $scope.content = '<div>Hello World</div>'
content: "="
},
template: '<div contentEditable="true"><p>Here is the value {{ content }}</p></div>'
});
Still not sure if I completely comprehend, but let me know if I'm getting closer.
I assume that you want to bind the HTML text in model to the element. I used ng-bind-html to render what is in the model and I created the directive ck-inline to add the inline feature and bind the model to the changes that happen in the inline editor. This directive requires a ng-bind-html to work and you also need to have ngSanitize added to your module. Add directive ck-inline to your element and
I also use $timeout because I noticed that if I don't the text is rendered and then ckeditor somehow deletes all the values which messes up the model (this does not happen with the non-inline option). Here is the code.
yourModule.directive('ckInline', ['$sce', '$timeout', function($sce, $timeout){
return{
require : '?ngBindHtml',
scope:{value:"=ngBindHtml"},
link : function(scope, elm, attr, ngBindHtml)
{
$timeout(function()
{
var ck_inline;
elm.attr("contenteditable", "true");
CKEDITOR.disableAutoInline = true;
ck_inline = CKEDITOR.inline(elm[0]);
if (!attr.ngBindHtml)
return;
ck_inline.on('instanceReady', function()
{
ck_inline.setData(elm.html());
});
function updateHtml()
{
scope.$apply(function()
{
scope.value = $sce.trustAsHtml(ck_inline.getData());
});
}
ck_inline.on('blur', updateHtml);
ck_inline.on('dataReady', updateHtml);
});
}
};
}]);
I'm a bit new to AngularJS and am trying to write a custom select control based on Zurb Foundation's custom select(see here: http://foundation.zurb.com/docs/components/custom-forms.html)
I know I need to use a directive for this but am not sure how to accomplish this.
It's going to have to be reusable and allow for the iterating of whatever array is passed in to it. A callback when the user selects the item from the dropdown list is probably needed.
Here is the markup for the custom Foundation dropdown list:
<select name="selectedUIC" style="display:none;"></select>
<div class="custom dropdown medium" style="background-color:red;">
Please select item
<ul ng-repeat="uic in uics">
<li class="custom-select" ng-click="selectUIC(uic.Name)">{{uic.Name}}</li>
</ul>
</div>
This works for now. I am able to populate the control from this page's Ctrl. However, as you can see, I'd have to do this every time I wanted to use a custom dropdown control.
Any ideas as to how I can turn this baby into a reusable directive?
Thanks for any help!
Chris
If you want to make your directives reusable not just on the same page, but across multiple AngularJS apps, then it's pretty handy to set them up in their own module and import that module as a dependency in your app.
I took Cuong Vo's plnkr above (so initial credit goes to him) and separated it out with this approach. Now this means that if you want to create a new directive, simply add it to reusableDirectives.js and all apps that already have ['reusableDirectives'] as a dependency, will be able to use that new directive without needing to add any extra js to that particular app.
I also moved the markup for the directive into it's own html template, as it's much easy to read, edit and maintain than having it directly inside the directive as a string.
Plnkr Demo
html
<zurb-select data-label="{{'Select an option'}}" data-options="names"
data-change-callback="callback(value)"></zurb-select>
app.js
// Add reusableDirectives as a dependency in your app
angular.module('angularjs-starter', ['reusableDirectives'])
.controller('MainCtrl', ['$scope', function($scope) {
$scope.names = [{name: 'Gavin'}, {name: 'Joseph'}, {name: 'Ken'}];
$scope.callback = function(name) {
alert(name);
};
}]);
reusableDirectives.js
angular.module('reusableDirectives', [])
.directive('zurbSelect', [function(){
return {
scope: {
label: '#', // optional
changeCallback: '&',
options: '='
},
restrict: 'E',
replace: true, // optional
templateUrl: 'zurb-select.html',
link: function(scope, element, attr) { }
};
}]);
zurb-select.html
<div class="row">
<div class="large-12 columns">
<label>{{label || 'Please select'}}</label>
<select data-ng-model="zurbOptions.name" data-ng-change="changeCallback({value: zurbOptions.name})"
data-ng-options="o.name as o.name for o in options">
</select>
</div>
</div>
Is something like this what you're looking for?
http://plnkr.co/edit/wUHmLP
In the above example you can pass in two attribute parameters to your custom zurbSelect directive. Options is a list of select option objects with a name attribute and clickCallback is the function available on the controller's scope that you want the directive to invoke when a user clicks on a section.
Notice there's no code in the link function (this is where the logic for your directive would generally go). All we're doing is wrapping a template so that it's reusable and accepts some parameters.
We created an isolated scope so the directive doesn't need to depend on parent scopes. We binded the isolated scope to the attribute parameters passed in. The '&' means bind to the expression on the parent scope calling this (in our case the callback function available in our controller) and the '=' means create a two way binding between the options attribute so when it changes in the outter scope, the change is reflected here and vice versa.
We're also restricting the usage of this directive to only elements (). You can set this to class, attributes, etc..
For more details the AngularJs directives guide is really good:
http://docs.angularjs.org/guide/directive
Hope this helps.
I've started working with angular js and have a problem that requires getting the current state of the DOM inside of my controller. Basically I'm building a text editor inside of an contenteditable div. Revisions to the text in the div can come from an external service(long polling pushes from the server) as well as the user actually typing in the field. Right now the revisions from the server are manipulating my angular model, which then updates the view through an ng-bind-html-unsafe directive. The only problem with this is that this blows away the users current cursor position and text selection.
I've figured out a way around the problem, but it requires directly manipulating dom elements in my controller, which seems to be discouraged in angular. I'm looking for either validation of my current method, or reccomendations on something more "angulary".
Basically what I've done is added two events to my model, "contentChanging" and "contentChanged". The first is fired right before I update the model, the second right after. In my controller I subscribe to these events like this.
//dmp is google's diff_match_patch library
//rangy is a selection management library http://code.google.com/p/rangy/wiki/SelectionSaveRestoreModule
var selectionPatch;
var selection;
scope.model.on("contentChanging", function() {
var currentText = $("#doc").html();
selection = rangy.saveSelection();
var textWithSelection = $("#doc").html();
selectionPatch = dmp.patch_make(currentText, textWithSelection);
});
scope.model.on("contentChanged", function() {
scope.$apply();
var textAfterEdit = $("#doc").html();
$("#doc").html(dmp.patch_apply(selectionPatch, textAfterEdit)[0]);
rangy.restoreSelection(selection);
});
So basically, when the content is changing I grab the current html of the editable area. Then I use the rangy plugin which injects hidden dom elements into the document to mark the users current position and selection. I take the html without the hidden markers and the html with the markers and I make a patch using google's diff_match_patch library(dmp).
Once the content is changed, I invoke scope.$apply() to update the view. Then I get the new text from the view and apply the patch from earlier, which will add the hidden markers back to the html. Finally I use range to restore the selection.
The part I don't like is how I use jquery to get the current html from the view to build and apply my patches. It's going to make unit testing a little tricky and it just doesn't feel right. But given how the rangy library works, I can't think of another way to do it.
Here's a simple example of how you would start:
<!doctype html>
<html ng-app="myApp">
<head>
<script src="http://code.angularjs.org/1.1.2/angular.min.js"></script>
<script type="text/javascript">
function Ctrl($scope) {
$scope.myText = "Here's some text";
}
angular.module("myApp", []).directive('texteditor', function() {
return {
restrict: 'E',
replace: true,
template: '<textarea></textarea>',
scope: {
text: '=' // link the directives scopes `text` property
// to the expression inside the text attribute
},
link: function($scope, elem, attrs) {
elem.val($scope.text);
elem.bind('input', function() {
// When the user inputs text, Angular won't know about
// it since we're not using ng-model so we need to call
// $scope.$apply() to tell Angular run a digest cycle
$scope.$apply(function() {
$scope.text = elem.val();
});
});
}
};
});
</script>
</head>
<body>
<div ng-controller="Ctrl">
<texteditor text="myText"></texteditor>
<p>myText = {{myText}}</p>
</div>
</body>
</html>
It's just binding to a textarea, so you would replace that with your real text editor. The key is to listen to changes on the text in your text editor, and update the value on your scope so that the outside world know that the user changed the text inside the text editor.