Is it not possible to set value from parent scope to contentEditable directive via isolated scope using two-way binding?
Code here: http://jsfiddle.net/bharatwaj/3wTd3/5
HTML:
<input ng-model="foo" />
<div contentEditable="true" binding-foo="foo" ng-model="input.name" title="Click to edit"></div>
<pre>model = {{input.name}}</pre>
JS:
angular.module('form-example2', []).directive('contenteditable', function () {
return {
scope: {
isolatedBindingFoo: '=bindingFoo'
},
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
console.log('isolatedBindingFoo value is ' + scope.isolatedBindingFoo);
// view -> model
elm.bind('blur', function () {
scope.$apply(function () {
ctrl.$setViewValue(scope.isolatedBindingfoo);
});
});
// model -> view
ctrl.$render = function () {
elm.html(ctrl.$viewValue);
};
// load init value from DOM
// Why is content editable value displayed as Hello!
// after setting view value below?
ctrl.$setViewValue(scope.isolatedBindingFoo);
}
};
});
function ItemCtl($scope) {
$scope.foo = 'Hello!';
}
I have a ng-model which is set to 'Hello!' in controller and is two-binding to content editable div
shouldn't contentEditable div display Hello!?
Note: It is possible to set by interpolate like below but i would like to know if it not possible to set via two-binding in scope.
{{foo}}
Related
I'm trying to store the value of a contenteditable to my JS code. But I can't find out why ng-model doesn't work in this case.
<div ng-app="Demo" ng-controller="main">
<input ng-model="inputValue"></input>
<div>{{inputValue}}</div> // Works fine with an input
<hr/>
<div contenteditable="true" ng-model="contentValue"></div>
<div>{{contentValue}}</div> // Doesn't work with a contenteditable
</div>
Is there a workaround to do that ?
See : JSFiddle
Note: I'm creating a Text editor, so the user should see the result, while I'm storing the HTML behind it. (ie. user see: "This is an example !", while I store: This is an <b>example</b> !)
contenteditable tag will not work directly with angular's ng-model because the way contenteditable rerender the dom element on every change.
You have to wrap it with a custom directive for that:
JS:
angular.module('customControl', ['ngSanitize']).
directive('contenteditable', ['$sce', function($sce) {
return {
restrict: 'A', // only activate on element attribute
require: '?ngModel', // get a hold of NgModelController
link: function(scope, element, attrs, ngModel) {
if (!ngModel) return; // do nothing if no ng-model
// Specify how UI should be updated
ngModel.$render = function() {
element.html($sce.getTrustedHtml(ngModel.$viewValue || ''));
};
// Listen for change events to enable binding
element.on('blur keyup change', function() {
scope.$evalAsync(read);
});
read(); // initialize
// Write data to the model
function read() {
var html = element.html();
// When we clear the content editable the browser leaves a <br> behind
// If strip-br attribute is provided then we strip this out
if ( attrs.stripBr && html == '<br>' ) {
html = '';
}
ngModel.$setViewValue(html);
}
}
};
}]);
HTML
<form name="myForm">
<div contenteditable
name="myWidget" ng-model="userContent"
strip-br="true"
required>Change me!</div>
<span ng-show="myForm.myWidget.$error.required">Required!</span>
<hr>
<textarea ng-model="userContent"></textarea>
</form>
Source it from the original docs
Just move the read function call into $render
angular.module('customControl', ['ngSanitize']).
directive('contenteditable', ['$sce', function($sce) {
return {
restrict: 'A', // only activate on element attribute
require: '?ngModel', // get a hold of NgModelController
link: function(scope, element, attrs, ngModel) {
if (!ngModel) return; // do nothing if no ng-model
// Specify how UI should be updated
ngModel.$render = function() {
element.html($sce.getTrustedHtml(ngModel.$viewValue || ''));
read(); // initialize
};
// Listen for change events to enable binding
element.on('blur keyup change', function() {
scope.$evalAsync(read);
});
// Write data to the model
function read() {
var html = element.html();
// When we clear the content editable the browser leaves a <br> behind
// If strip-br attribute is provided then we strip this out
if ( attrs.stripBr && html == '<br>' ) {
html = '';
}
ngModel.$setViewValue(html);
}
}
};
}]);
Neither of the other answers worked for me. I needed the model's initial value to be rendered when the control was initialized. Instead of calling read(), I used this code inside the link function:
ngModel.$modelValue = scope.$eval(attrs.ngModel);
ngModel.$setViewValue(ngModel.$modelValue);
ngModel.$render()
In this example, I have two AngularJS KendoDatePickers. The first one works perfectly, if you click on the button you open the calendar. The second one is within a directive that has the transclude attribute set to true. If you click on the second button, you get an error.
My understanding is that the scope of the transcluded portion inherits from the control scope, so this should work. Where am I wrong?
This is the plunk
HTML
<input kendo-date-picker="picker" />
<button ng-click="openPicker()">Open Date Picker</button>
<my-window>
<input kendo-date-picker="picker2" />
<button ng-click="openPicker2()">Open Date Picker 2</button>
</my-window>
Javascript
var app = angular.module("app", [ "kendo.directives" ]);
app.controller('MyCtrl', function($scope) {
$scope.openPicker = function () {
$scope.picker.open();
};
$scope.openPicker2 = function () {
$scope.picker2.open();
};
});
app.directive('myWindow', function() {
return {
transclude: true,
scope: {
someVar: '='
},
restrict: 'EA',
template: function(element, attrs) {
var html = '<div ng-transclude></div>';
return html;
}
};
});
There are two things about your code:
first: you create an isolatedScope so you do not have access to the controller scope inside the directive scope.
second: transcluded content get their own scope. One way to work around this is by not using transclude at all, like the example below:
return {
transclude: false,
restrict: 'EA',
template: function(element, attrs) {
var html = '<div>'+element.html()+'</div>';
return html;
}
or use the link function and manually transclude the element with the scope of the directive
After looking for examples of how set focus elements with angular, I saw that most of them use some variable to watch for then set focus, and most of them use one different variable for each field they want to set focus. In a form, with a lot of fields, that implies in a lot of different variables.
With jquery way in mind, but wanting to do that in angular way, I made a solution that we set focus in any function using the element's id, so, as I am very new in angular, I'd like to get some opinions if that way is right, have problems, whatever, anything that could help me do this the better way in angular.
Basically, I create a directive that watch a scope value defined by the user with directive, or the default's focusElement, and when that value is the same as the element's id, that element set focus itself.
angular.module('appnamehere')
.directive('myFocus', function () {
return {
restrict: 'A',
link: function postLink(scope, element, attrs) {
if (attrs.myFocus == "") {
attrs.myFocus = "focusElement";
}
scope.$watch(attrs.myFocus, function(value) {
if(value == attrs.id) {
element[0].focus();
}
});
element.on("blur", function() {
scope[attrs.myFocus] = "";
scope.$apply();
})
}
};
});
An input that needs to get focus by some reason, will do this way
<input my-focus id="input1" type="text" />
Here any element to set focus:
<a href="" ng-click="clickButton()" >Set focus</a>
And the example function that set focus:
$scope.clickButton = function() {
$scope.focusElement = "input1";
}
Is that a good solution in angular? Does it have problems that with my poor experience I don't see yet?
The problem with your solution is that it does not work well when tied down to other directives that creates a new scope, e.g. ng-repeat. A better solution would be to simply create a service function that enables you to focus elements imperatively within your controllers or to focus elements declaratively in the html.
DEMO
JAVASCRIPT
Service
.factory('focus', function($timeout, $window) {
return function(id) {
// timeout makes sure that it is invoked after any other event has been triggered.
// e.g. click events that need to run before the focus or
// inputs elements that are in a disabled state but are enabled when those events
// are triggered.
$timeout(function() {
var element = $window.document.getElementById(id);
if(element)
element.focus();
});
};
});
Directive
.directive('eventFocus', function(focus) {
return function(scope, elem, attr) {
elem.on(attr.eventFocus, function() {
focus(attr.eventFocusId);
});
// Removes bound events in the element itself
// when the scope is destroyed
scope.$on('$destroy', function() {
elem.off(attr.eventFocus);
});
};
});
Controller
.controller('Ctrl', function($scope, focus) {
$scope.doSomething = function() {
// do something awesome
focus('email');
};
});
HTML
<input type="email" id="email" class="form-control">
<button event-focus="click" event-focus-id="email">Declarative Focus</button>
<button ng-click="doSomething()">Imperative Focus</button>
About this solution, we could just create a directive and attach it to the DOM element that has to get the focus when a given condition is satisfied. By following this approach we avoid coupling controller to DOM element ID's.
Sample code directive:
gbndirectives.directive('focusOnCondition', ['$timeout',
function ($timeout) {
var checkDirectivePrerequisites = function (attrs) {
if (!attrs.focusOnCondition && attrs.focusOnCondition != "") {
throw "FocusOnCondition missing attribute to evaluate";
}
}
return {
restrict: "A",
link: function (scope, element, attrs, ctrls) {
checkDirectivePrerequisites(attrs);
scope.$watch(attrs.focusOnCondition, function (currentValue, lastValue) {
if(currentValue == true) {
$timeout(function () {
element.focus();
});
}
});
}
};
}
]);
A possible usage
.controller('Ctrl', function($scope) {
$scope.myCondition = false;
// you can just add this to a radiobutton click value
// or just watch for a value to change...
$scope.doSomething = function(newMyConditionValue) {
// do something awesome
$scope.myCondition = newMyConditionValue;
};
});
HTML
<input focus-on-condition="myCondition">
I like to avoid DOM lookups, watches, and global emitters whenever possible, so I use a more direct approach. Use a directive to assign a simple function that focuses on the directive element. Then call that function wherever needed within the scope of the controller.
Here's a simplified approach for attaching it to scope. See the full snippet for handling controller-as syntax.
Directive:
app.directive('inputFocusFunction', function () {
'use strict';
return {
restrict: 'A',
link: function (scope, element, attr) {
scope[attr.inputFocusFunction] = function () {
element[0].focus();
};
}
};
});
and in html:
<input input-focus-function="focusOnSaveInput" ng-model="saveName">
<button ng-click="focusOnSaveInput()">Focus</button>
or in the controller:
$scope.focusOnSaveInput();
angular.module('app', [])
.directive('inputFocusFunction', function() {
'use strict';
return {
restrict: 'A',
link: function(scope, element, attr) {
// Parse the attribute to accomodate assignment to an object
var parseObj = attr.inputFocusFunction.split('.');
var attachTo = scope;
for (var i = 0; i < parseObj.length - 1; i++) {
attachTo = attachTo[parseObj[i]];
}
// assign it to a function that focuses on the decorated element
attachTo[parseObj[parseObj.length - 1]] = function() {
element[0].focus();
};
}
};
})
.controller('main', function() {});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.min.js"></script>
<body ng-app="app" ng-controller="main as vm">
<input input-focus-function="vm.focusOnSaveInput" ng-model="saveName">
<button ng-click="vm.focusOnSaveInput()">Focus</button>
</body>
Edited to provide more explanation about the reason for this approach and to extend the code snippet for controller-as use.
You can try
angular.element('#<elementId>').focus();
for eg.
angular.element('#txtUserId').focus();
its working for me.
Another option would be to use Angular's built-in pub-sub architecture in order to notify your directive to focus. Similar to the other approaches, but it's then not directly tied to a property, and is instead listening in on it's scope for a particular key.
Directive:
angular.module("app").directive("focusOn", function($timeout) {
return {
restrict: "A",
link: function(scope, element, attrs) {
scope.$on(attrs.focusOn, function(e) {
$timeout((function() {
element[0].focus();
}), 10);
});
}
};
});
HTML:
<input type="text" name="text_input" ng-model="ctrl.model" focus-on="focusTextInput" />
Controller:
//Assume this is within your controller
//And you've hit the point where you want to focus the input:
$scope.$broadcast("focusTextInput");
I prefered to use an expression. This lets me do stuff like focus on a button when a field is valid, reaches a certain length, and of course after load.
<button type="button" moo-focus-expression="form.phone.$valid">
<button type="submit" moo-focus-expression="smsconfirm.length == 6">
<input type="text" moo-focus-expression="true">
On a complex form this also reduces need to create additional scope variables for the purposes of focusing.
See https://stackoverflow.com/a/29963695/937997
I have created a directive for auto focus on text box
(function () {
'use strict';
angular.module('commonModule').directive('srFocuson',function(){
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs) {
scope.$watch(attrs.focusMe, function (value) {
if (value === true) {
console.log('value=', value);
element[0].focus();
scope[attrs.focusMe] = false;
}
});
}
};
});
})();
And now i want to bind that directive to my text box.I have tried to bind to input field but its not working.
<input placeholder="SR ID, SSN/ITIN, or School ID" sr-focuson="focusMe" type="text"
id="form_ID" name="searchId" autofocus
data-ng-model="vm.searchCriteria.searchId"
maxlength="20" class="form-control">
http://plnkr.co/edit/A39duXhGvCedAaVuB3uQ?p=preview
I made working fiddle with your idea. http://jsfiddle.net/fLaAG/
It's sort of unclear where you would be updating scope.focusMe so I made an explicit button that would set that value to true.
<button type="button" ng-click="Focus()" type="button">Focus</button>
...
$scope.Focus = function() {
$scope.focusMe = true;
};
Also I'm setting up an isolate scope, so I can just watch string I give it.
scope: {
focusMe: '=focusOn'
},
Hope this helps
Here is a method using built-in angular functionality, dug out from the murky depths of the angular docs. Notice how the "link" attribute can be split into "pre" and "post", for pre-link and post-link functions.
Working Example: http://plnkr.co/edit/Fj59GB
// this is the directive you add to any element you want to highlight after creation
Guest.directive('autoFocus', function() {
return {
link: {
pre: function preLink(scope, element, attr) {
console.debug('prelink called');
// this fails since the element hasn't rendered
//element[0].focus();
},
post: function postLink(scope, element, attr) {
console.debug('postlink called');
// this succeeds since the element has been rendered
element[0].focus();
}
}
};
});
<input value="hello" />
<!-- this input automatically gets focus on creation -->
<input value="world" auto-focus />
Full AngularJS Directive Docs: https://docs.angularjs.org/api/ng/service/$compile
I have an anchor tag that I wish to hide or show depending on a value in the model.
<table>
<tr ng-repeat="item in items">
<td>Other Stuff</td>
<td>
<a href="#/somewhere" ng-show="model.showIt" myCustomDir="some value" onClick="bar(item)" />
</td>
</tr>
</table>
Now in my directive I have the following:
app.directive('myCustomDir', function() {
var def = {
restrict: 'A',
scope: {
onClick: "&"
},
link: function(scope, element, attrs) {
var hover = angular.element("<div><b>Some Text</b></div>");
var button = hover.find('b');
button.on('click', function() {
scope.$apply(function() {
scope.onClick();
})
});
}
};
return def;
})
The problem is as soon as I include my directive the ng-show one I think no longer works and that is because if I am correct it is because my directive works in isolate scope so the model from the parent scope is no longer present.
How would I get my directive to play nicely with ng-show while still being able to let someone what method they want to call when the tag is clicked.
Plunker for all those interested. http://plnkr.co/edit/BLMCgB
You directive creates an isolated scope. So you need to use $parent to get the value of the current repeater item
ng-show="$parent.item.visible"
If you want to make it more generic, you can take the scope off to make it compatible with other directives. Then you can use scope.$eval to call the function passed in.
myApp.directive('myDirective', function ($document) {
var definition = {
restrict: 'A',
link: function (scope, element, attrs) {
element.on('click', function () {
...
button.on('click', function () {
scope.$apply(function () {
scope.$eval(attrs.onClick);
hover.remove();
})
});
});
}
}
return definition;
})
If you want allow any global directive - don't declare private scope.
If you want allow only few directives, add links in scope declaration:
scope: {
onClick: "&",
ngShow : "&"
},
To your question in comments:
Declare controller in directive and declare method in this controller. Then in directive template assign ng-click to this method.
var def = {
restrict: 'A',
controller: function($scope){
$scope.callMe = function(){
console.log('foo');
}
}
}
in template:
<div ng-click="callMe()">content</div>
This method will be accessible only inside your directive.