Get original transcluded content within angular directive - javascript

My goal is to create an editable directive that allows a user to edit HTML of any element to which the attribute is attached (see Plunker: http://plnkr.co/edit/nIrr9Lu0PZN2PdnhQOC6)
This almost works except I can't get the original raw HTML of the transcluded content to initialize the text area. I can get the text of it from clone.text(), but that's missing the HTML tags like <H1>, <div>, etc. so clicking apply with no edits is not idempotent.
The method clone.html() throws an error, Cannot read property 'childNodes' of undefined
app.directive("editable", function($rootScope) {
return {
restrict: "A",
templateUrl: "mytemplate.html",
transclude: true,
scope: {
content: "=editContent"
},
controller: function($scope, $element, $compile, $transclude, $sce) {
// Initialize the text area with the original transcluded HTML...
$transclude(function(clone, scope) {
// This almost works but strips out tags like <h1>, <div>, etc.
// $scope.editContent = clone.text().trim();
// this works much better per #Emmentaler, tho contains expanded HTML
var html = "";
for (var i=0; i<clone.length; i++) {
html += clone[i].outerHTML||'';}
});
$scope.editContent = html;
$scope.onEdit = function() {
// HACK? Using jQuery to place compiled content
$(".editable-output",$element).html(
// compiling is necessary to render nested directives
$compile($scope.editContent)($rootScope)
);
}
$scope.showEditor = false;
$scope.toggleEditor = function() {
$scope.showEditor = !$scope.showEditor;
}
}
}
});
(This question is essentially a wholesale rewrite of the question and code after an earlier attempt to frame the question, Get original transcluded content in Angular directive)

The $element.innerHTML should contain the original HTML. I am showing that it contains
<div class="editable">
<span class="glyphicon glyphicon-edit" ng-click="toggleEditor()"></span>
<div class="editable-input" ng-show="showEditor">
<b><p>Enter well-formed HTML content:</p></b>
<p>E.g.<code><h1>Hello</h1><p>some text</p><clock></clock></code></p>
<textarea ng-model="editContent"></textarea>
<button class="btn btn-primary" ng-click="onEdit()">apply</button>
</div>
<div class="editable-output" ng-transclude=""></div>
</div>

Related

angularjs - get ng-repeat element using element.html() inside a jsPanel directive

I am trying to create a directive which will take the html content and place it n a jsPanel element.
So far I am able to compile the scope and able get elements with the scope. Then i tried to get html tag with ng-repeat, where i got stuck.
Following is the code used to create the directive
mainApp.directive('jspanelcontent', function($compile) {
console.log("loading");
var directive = {};
directive.restrict = 'AE';
directive.compile = function(element, attributes) {
var linkFunction = function($scope, element, attributes) {
console.log(element.html());
var contenthtml = element.html();
contenthtml = angular.element($compile(contenthtml)($scope));
element.html("");
$scope.jspanleinstance = $.jsPanel({
position: {
my: "left-bottom",
at: "left-bottom",
offsetY: 15
},
theme: "rebeccapurple",
contentSize: {
width: 600,
height: 350
},
headerTitle: attributes.panelcaption,
content: contenthtml,
});
// });
}
return linkFunction;
}
return directive;
});
And using like following in html
<div jspanelcontent panelcaption="list">
<div ng-controller="ListController">
{{test}}
<div ng-repeat="user in users">
<span>{{user.id}}</span>
<span>{{user.name}}</span>
<span>{{user.email}}</span>
<br />
</div>
</div>
The console log returns as
Output i am getting (jsPanel)
As you see the "test" variable is properly bind and the ng-repeat element is display as commented, I am not aware why it's returning like that. If anyone can figure out the issue, then i might get it working.
I know i can access the users data as $scope.users inside the directive. The problem here is that user able to use the directive commonly, so i have to assume that i don't the variables in the $scope.
I stuck in this place, and couldn't find any solutions to try. Any suggestions or solutions will be more helpful. :)
NOTE: No syntax errors, outside the directive the data is displaying (Tested)

AngularJs Directive update

I want to update my directive content only at some desired places, but not at others. I have simulated my problem here:
http://jsfiddle.net/Lvc0u55v/2945/
The problem is, I have an 'editor' directive which is applied in two places:
<span class="editor1" editor ></span>
<span class="editor2" editor ></span>
I want to update the content of span class="editor1" on button click.
How do I do it?
Why not go with a relatively Angularesque approach by isolating the scope of the directive and to a maximum extent, avoid jQuery in your logic.
So you could have your directive defined as such:
.directive('editor', function() {
return {
scope: {
upd : '=',
editordata : '=data'
},
template: '<div>{{editordata}}</div>',
controller: function($scope, $rootScope, $element) {
$rootScope.$on('update', function(evt, data) {
if(data.upd === $scope.upd){
$scope.editordata = data.txt;
}
})
},
link: function(scope, el, attr) {}
}
})
Here, you are passing the required information which the editor directive depends upon through its scope, namely upd (which I suppose is how you want to uniquely identify the items by) and the text data.
Meanwhile, you can define a list of the editor items in the common parent controller MyCtrl and iterate over them in the DOM with ng-repeat.
// MyCtrl controller
$scope.list = [
{upd: 'editor1', data: 'original data for editor1'},
{upd: 'editor1', data: 'original data for editor2'}
]
<!-- HTML -->
<div ng-repeat="item in list" upd="item.upd" data="item.data"></div>
Demo
You can check if the current directory has the "editor1" class and if so complete your logic.
You can look at this example :
element[0].querySelector('.editor1') !== undefined'

How to pass a DOM element to "ng-disabled"

All,
How can I pass the current DOM element to the Angular directive "ng-disabled"?
I do know that messing w/ the DOM in Angular is bad practice. But I can't think of another - simple - way to do this. Here is my problem:
I have a button that updates a scope variable when clicked:
<button ng-click="target_model.display_detail=true">click me</button>
Elsewhere in my template there is code that watches "target_model.display_detail" - when it is true it displays a modal-dialog which includes an Angular directive which gets some data from the server and populates a form which includes another button like the one above.
The data structure that I am working w/ is potentially recursive; there are loads of nested "target_models". So it is possible for a button in a modal-dialog to point a target_model whose form has already been created. In that case, I just want to disable the button. I'd like todo something like:
<button ng-disabled="ancestor_model_exists(the_current_element, target_model.some_unique_id)">click me</button>
Where "ancestor_model_exists" is a function that would check the DOM to see if there is an ancestor element with a particular id. But how do I know which element to start from?
You're approaching DOM manipulations imperatively - the jQuery way, not declaratively - the Angular way.
DOM manipulation is fine... inside directives. You don't do it in controllers, where you presumably defined that function.
When you get a chance, try to get away with 0 calls to $ in a sandbox to force you to learn how to do things the Angular way - not because it's "better" in an absolute way - it's just generally better to first learn the toolkit and recommended approaches before doing it your way, anyway.
This should do what you want, except maybe searching beyond multiple ancestors (but I mention how to do that if you need that):
https://plnkr.co/edit/7O8UDuqsVTlH8r2GoxQu?p=preview
JS
app.directive('ancestorId', function() {
return {
restrict: 'A',
controller: 'AncestorIdController',
require: ['ancestorId'],
link: function(scope, element, attrs, controllers) {
var ancestorIdController = controllers[0];
// If you wanted to use an expression instead of an
// interpolation you could define an isolate scope on this
// directive and $watch it.
attrs.$observe('ancestorId', function(value) {
ancestorIdController.setId(value);
});
}
}
});
app.controller('AncestorIdController', function() {
this.getId = _getId;
this.setId = _setId;
var id;
function _getId() {
return id;
}
function _setId(value) {
id = value;
}
});
app.directive('disableForAncestorId', function() {
return {
restrict: 'A',
require: ['?^ancestorId'],
link: function(scope, element, attrs, controllers) {
var ancestorIdController = controllers[0];
// Check to make sure the ancestorId is a parent.
if (ancestorIdController) {
scope.$watch(function() {
var watch = {
target: ancestorIdController.getId(),
actual: attrs.disableForAncestorId
};
return watch;
}, function(value) {
if (value.target === value.actual) {
element.attr('disabled', 'disabled');
} else {
element.removeAttr('disabled');
}
}, true /* Deep watch */ );
}
}
}
});
HTML
<!-- The simple happy path. -->
<div ancestor-id="A">
<button disable-for-ancestor-id="A">'A' === 'A' ?</button>
</div>
<!-- require will match the 'B' before the 'A' because it's closer.
if you need to match any parent you could use a third coordinating
directive. -->
<div ancestor-id="A">
<div ancestor-id="B">
<button disable-for-ancestor-id="A">'B' === 'A' ?</button>
</div>
</div>
<!-- require lets you freely change the DOM to add extra elements separating
you from what you're looking for.-->
<div ancestor-id="B">
<div>
<div>
<button disable-for-ancestor-id="B">'B' === 'B' ?</button>
</div>
</div>
</div>
<!-- It doesn't blow up if it doesn't find an ancestorId. -->
<div>
<button disable-for-ancestor-id="B">'B' === undefined ?</button>
</div>
<br>
Dynamic AncestorId test (it will be disabled if the text fields are equal):
<br>
Target AncestorId <input ng-model="targetAncestorId">
<br>
Actual Ancestor <input ng-model="actualAncestorId">
<!-- It doesn't blow up if it doesn't find an ancestorId. -->
<div ancestor-id="{{ targetAncestorId }}">
<button disable-for-ancestor-id="{{ actualAncestorId }}">'{{ actualAncestorId }}' === '{{ actualAncestorId }}' ?</button>
</div>
It never fails... posting a question on Stack Overflow always makes me realize the answer just minutes later.
The following code gets me pretty close:
template.html:
<button ng-click="display_if_ancestor_model_exists($event, target_model)">click me</button>
app.js:
$scope.display_if_ancestor_model_exists = function($event, target_model) {
var ancestor_form = $($event.target).closest("#" + target_model.some_unique_id);
if (ancestor_form.length) {
/* don't show the modal-dialog */
display_msg("This form already exists!");
}
else {
target_model.display_detail = true;
}
};
Of course, I would rather the button just be disabled but I can live w/ this solution for now.

angular directive: switch between two templates dynamically

I am trying to create a directive named availableTo that can switch between two different templates depending on some message. For example, if the field is an input with the ng-model directive, I would first need to change it to read-only using the <span> tag. So far, my code can switch the view to read-only, but I cannot seem to switch it back to input:
var directive = {
restrict: 'A',
require: '?ngModel',
link: linkerFn,
replace: true
};
function linkerFn(scope, element, attrs, ngModelCtrl) {
var clonedElement = angular.copy(element);
var preOuterHTML = clonedElement[0].outerHTML; //this can save the <input> field html code
scope.$on('mode_changed', function() {
var curUserRole = userservices.getUserRole();
if (attrs.availableTo == curUserRole) {
var e = $compile(preOuterHTML)(scope);
element.replaceWith(e);
} else {
var template = '<span>' + ngModelCtrl.$viewValue + '</span>';
var e = $compile(template)(scope);
element.replaceWith(e);
}
}); //scope.$on
} //linkerFn
For an input field:
<input name="test1" class="form-control" ng-model="name" placeholder="Name 1" available-to="ADMIN"/>
I also noticed that once I change the template in the else block above, the element re-renders, and the preOuterHTML does not contain the original element html any more. This seems to be mission impossible to me, but I would like to hear some expert opinions. Thanks
element.replaceWith(e); Don't do that. In Angular, if you find yourself attempting to modify the DOM directly, you are by definition doing it wrong. You gotta sit back and let Angular do the work.
If you need to replace a directive's entire template, a fairly straightforward approach is to use ng-include with a scope variable containing the desired conditional templateUrl, e.g.
var directive = {
// ...
template: '<div ng-include="myTemplateUrl"></div>',
link: function(scope, el) {
if (/* whatever */) {
scope.myTemplateUrl="templates/foo.html";
} else {
//...etc
}
},
};
(This does add an extra DOM node to the tree, but that's generally harmless.)
It sounds like in your case you may not need to go that far, though; a simple ng-if inside your template is probably enough to swap between your read-only <span> and <input>.

AngularJS ng-switch without extra DOM

I have a custom directive and an object myObj on the current $scope (inside an ng-repeat).
If the object has a type of html, I want to use one template:
<span ng-bind-html="myObj.html"></span>`
Otherwise I want to use a different template:
<span>{{myObj.value}}</span>`
My problem
This is invalid because a custom directive template must contain exactly one root node:
<span ng-if="myObj.type==='html'" ng-bind-html="myObj.html"></span>
<span ng-if="myObj.type!=='html'">{{myObj.value}}</span>
This is invalid because it destroys my page with extra DOM: (wrapping all my spans (there could be thousands) in unnecessary ng-switch nodes...)
<ng-switch on="myObj.type">
<span ng-switch-when="html" ng-bind-html="myObj.html"></span>
<span ng-switch-default>{{myObj.value}}</span>
</ng-switch>
My Question
Is it possible to have a directive pick it's template based on the result of a switch, without creating extra unnecessary DOM? For example, you can specify replace: true when creating a directive - is it possible to similarly have an ng-switch where the result replaces the switch tag itself?
Edit
My Directive:
return {
replace: true,
controller: 'ChunkController',
scope: {
chunk: '=deChunk'
},
templateUrl: de.partial.chunk,
link: function (scope, el, attr, ctrl) {
el.on('keydown', handleKeypress.bind(ctrl));
el.on('click', ctrl.showValue);
}
};
And its usage:
<div class="content" contenteditable="{{node.type!=='static'}}">
<div data-ng-repeat="chunk in node.chunks" data-de-chunk="chunk"></div>
</div>
With the intent that the child <div> will be replaced with the sequence of <span>s from above.
I wouldn't even bother if you are storing the html in a service just check to see if a value for myObj.html exists in the object and if it does compile and bind the html in the linker function instead of using ng-bind-html
something like this maybe:
myapp.directive('something',function($compile){
return {
link: function(scope,elem,attrs) {
var obj = scope.$eval(attrs.something);
if(obj.html) {
var html = angular.element($compile(obj.html)(scope));
elem.append(html);
} else {
//go get the data and set obj.html
}
}
}
});

Categories