I stumbled upon this article on how to build a click to edit feature for a form. The author states:
What about if you wanted input type="date" or even a select? This
is where you could add some extra attribute names to the directive’s
scope, like fieldType, and then change some elements in the template
based on that value. Or for full customisation, you could even turn
off replace: true and add a compile function that wraps the necessary
click to edit markup around any existing content in the page.
While looking through the code I cannot seem to wrap my head around how I could manipulate the template in such a way that I could make it apply to any angular component, let alone how I can make it apply to a drop down list. Code from article below:
app.directive("clickToEdit", function() {
var editorTemplate = '<div class="click-to-edit">' +
'<div ng-hide="view.editorEnabled">' +
'{{value}} ' +
'<a ng-click="enableEditor()">Edit</a>' +
'</div>' +
'<div ng-show="view.editorEnabled">' +
'<input ng-model="view.editableValue">' +
'Save' +
' or ' +
'<a ng-click="disableEditor()">cancel</a>.' +
'</div>' +
'</div>';
return {
restrict: "A",
replace: true,
template: editorTemplate,
scope: {
value: "=clickToEdit",
},
controller: function($scope) {
$scope.view = {
editableValue: $scope.value,
editorEnabled: false
};
$scope.enableEditor = function() {
$scope.view.editorEnabled = true;
$scope.view.editableValue = $scope.value;
};
$scope.disableEditor = function() {
$scope.view.editorEnabled = false;
};
$scope.save = function() {
$scope.value = $scope.view.editableValue;
$scope.disableEditor();
};
}
};
});
My question is, how can we extend the above code to allow for drop down edits? That is being able to change to the values that get selected.
One approach you might consider is using template: function(tElement,tAttrs ).
This would allow you to return appropriate template based on attributes.
app.directive("clickToEdit", function() {
return {
/* pseudo example*/
template: function(tElement,tAttrs ){
switch( tAttrs.type){
case 'text':
return '<input type="text"/>';
break;
}
},....
This is outlined in the $compile docs
Related
Im having some troubles with angular.
I need to generate a form using a JSON Spec built by a foreign system (the source is trustable).
First I got a problem with FormController because it didn't detect the elements that I was generating through a directive, then I did a workaround by generating the form and fields at the same time into the directive
The problem is that its quite messy as you can see in this JSFiddle.
var $form = $('<form/>', {
name: formName,
id: formName,
novalidate: true,
"ng-submit": 'daForm.validate($event, ' + formName + ')'
});
var idx = 1;
for (var fieldName in spec.fieldset) {
var $wrapper = $('<div/>', {
id: fieldName + '-col'
}).addClass('col-xs-12 col-md-6').appendTo($form);
var $formGroup = $('<div/>', {
id: fieldName + '-group'
}).addClass('form-group').appendTo($wrapper)
$('<label/>', {
'for': fieldName,
id: fieldName + '-label'
}).addClass('control-label').text("{{'" + fieldName + "' }}").appendTo($formGroup);
var fieldSpec = spec.fieldset[fieldName];
var control;
switch (fieldSpec.control) {
case 'passwordbox':
control = 'input';
fieldSpec.attrs.type = "password"
break;
case 'number':
control = 'input';
fieldSpec.attrs.type = "numberbox"
break;
case 'email':
control = 'input';
control = 'input';
fieldSpec.attrs.type = "emailbox"
break;
case 'select':
control = 'select';
break;
case 'textarea':
control = 'multitextbox';
break;
case 'textbox':
$('<da-textbox/>').attr('defined-by', fieldName).appendTo($formGroup)
continue;
break;
default:
control = 'input';
fieldSpec.attrs.type = "text"
break;
}
var $control = $('<' + control + '/>', fieldSpec.attrs).attr('ng-model', 'model.' + fieldName).addClass('form-control').appendTo($formGroup);
for (var rule in fieldSpec.validation) {
$control.attr(rule, fieldSpec.validation[rule])
}
if (control == 'select') {
for (var val in fieldSpec.options) {
$('<option/>').val(val).text(fieldSpec.options[val]).appendTo($control);
}
}
if (idx % 2 == 0)
$wrapper.parent().append($('<div/>').addClass('clearfix'))
idx++;
}
$form.append($('<div/>').addClass('clearfix'))
var $lastRow = $('<div/>').addClass('col-xs-12 col-md-12').appendTo($form);
var $submit = $('<button/>').attr('type', 'submit').addClass('btn btn-primary').appendTo(
$lastRow).text('Submit')
$form.append($('<div/>').addClass('clearfix'))
console.log(scope)
$compile($form)(scope);
element.append($form);
Notice that the case textbox is where my code fails, for everyother field I generate a plain input/select/textarea field and push it to the container. In textbox case I try to push a new directive in order to tidy up this mess a little bit, but the FormController doesn't recognize it as the other plain items.
Any ideas on how can I make angular recognize the field generated by my new directive?
Addenda
1.- ngModel works fine, it updates correctly.
2.- Updated JSFiddle
OK, got it. You are mixing jQuery and AngularJS which often leads to some wonkiness, so it took me a minute to get what was happening. So, I was right, the daTextbox element wasn't being bound to the $scope (you can tell by looking at the class list, if it doesn't contain .ng-scope, something is wrong).
Any way, the first think you want to do is make sure the daTextbox will be compiled earlier than ngModel (so any priority greater than 0, I chose 1000). Then, instead of creating the input with jQuery, it is much easier and more efficient to use angular.element.
What you will want to do is to create the input, compile it, then append it to your directive element. Here's a working example:
app.directive('daTextbox', ['$compile', function($compile) {
return {
restrict: 'E',
scope: true,
priority: 1000,
controller: function($scope, $attrs) {
this.identifier = $attrs.definedBy
this.definition = $scope.spec.fieldset[this.identifier]
},
link: function(scope, element, attrs, controller) {
var input = angular.element('<input/>')
.addClass('form-control')
.attr('ng-model', 'model.' + attrs.definedBy);
input = $compile(input)(scope);
element.append(input);
}
};
}]);
Doing it this way, I'm guessing your controller isn't necessary, but it might still be. At any rate, you can see this is working on plnkr.
I decided to write a custom directive to help me validate my input boxes. The idea is that I add my new fancy nx-validate directive to a bootstrap div.form-group and it'll check whether my <input/> is $dirty or $invalid and apply the .has-success or .has-error class as required.
For some odd reason, my directive works perfectly under normal circumstances, but the added ng-class is completely ignored inside a ui-bootstrap modal.
Identical code in both the modal and the form
<form name="mainForm">
<div class="row">
<div nx-validate class="form-group has-feedback col-lg-6 col-md-6 col-xs-12">
<label class="control-label">Green if long enough, red if not</label>
<input type="text" id="name" class="form-control" ng-model="property.name" required="required" ng-minlength="5"/>
(once touched I do change colour - happy face)
</div>
</div>
And my lovely directive
nitro.directive("nxValidate", function($compile) {
return {
restrict: 'A',
priority: 2000,
compile: function(element) {
var node = element;
while (node && node[0].tagName != 'FORM') {
console.log (node[0].tagName)
node = node.parent();
}
if (!node) console.error("No form node as parent");
var formName = node.attr("name");
if (!formName) console.error("Form needs a name attribute");
var label = element.find("label");
var input = element.find("input");
var inputId = input.attr("id")
if (!label.attr("for")) {
label.attr("for", inputId);
}
if (!input.attr("name")) {
input.attr("name", inputId);
}
if (!input.attr("placeholder")) {
input.attr("placeholder", label.html());
}
element.attr("ng-class", "{'has-error' : " + formName + "." + inputId + ".$invalid && " + formName + "." + inputId + ".$touched, 'has-success' : " + formName + "." + inputId + ".$valid && " + formName + "." + inputId + ".$touched}");
element.removeAttr("nx-validate");
var fn = $compile(element);
return function($scope) {
fn($scope);
}
}
}
});
Check it out on plunker: http://plnkr.co/edit/AjvNi5e6hmXcTgpXgTlH?
The simplest way I'd suggest you is you can put those classes by using watch on those fields, this watcher will lie inside the postlink function after compiling a DOM
return function($scope, element) {
fn($scope);
$scope.$watch(function(){
return $scope.modalForm.name.$invalid && $scope.modalForm.name.$touched;
}, function(newVal){
if(newVal)
element.addClass('has-error');
else
element.removeClass('has-error');
})
$scope.$watch(function(){
return $scope.modalForm.name.$valid && $scope.modalForm.name.$touched;
}, function(newVal){
if(newVal)
element.addClass('has-success');
else
element.removeClass('has-success');
})
}
Demo Here
Update
The actual better way of doing this would be instead of compiling element from compile, we need $compile the element from the link function itself. The reason behind the compiling DOM in link fn using $compile is that our ng-class attribute does contain the scope variable which is like myForm.name.$invalid ,so when we $compile the DOM of compile function then they are not evaluating value of myForm.name.$invalid variable because compile don't have access to scope & the would be always undefined or blank. So while compile DOM inside the link would have all the scope values are available that does contain myForm.name.$invalid so after compiling it with directive scope you will get your ng-class directive binding will work.
Code
compile: function(element) {
//..other code will be as is..
element.removeAttr("nx-validate");
//var fn = $compile(element); //remove this line from compile fn
return function($scope, element) {
//fn($scope);
$compile(element)($scope); //added in postLink to compile dom to get binding working
}
}
Updated Plunkr
I have the following directive that my application is using. I was under the impression that my application was working fine with AngularJS 1.3 but after a lot of changes including a move to the latest version, the removal of jQuery, and also the use of controller as then now this directive is giving me errors:
app.directive('pagedownAdmin', function ($compile, $timeout) {
var nextId = 0;
var converter = Markdown.getSanitizingConverter();
converter.hooks.chain("preBlockGamut", function (text, rbg) {
return text.replace(/^ {0,3}""" *\n((?:.*?\n)+?) {0,3}""" *$/gm, function (whole, inner) {
return "<blockquote>" + rbg(inner) + "</blockquote>\n";
});
});
return {
require: 'ngModel',
replace: true,
scope: {
modal: '=modal'
},
template: '<div class="pagedown-bootstrap-editor"></div>',
link: function (scope, iElement, attrs, ngModel) {
var editorUniqueId;
if (attrs.id == null) {
editorUniqueId = nextId++;
} else {
editorUniqueId = attrs.id;
}
var newElement = $compile(
'<div>' +
'<div class="wmd-panel">' +
'<div data-ng-hide="modal.wmdPreview == true" id="wmd-button-bar-' + editorUniqueId + '"></div>' +
'<textarea data-ng-hide="modal.wmdPreview == true" class="wmd-input" id="wmd-input-' + editorUniqueId + '">' +
'</textarea>' +
'</div>' +
'<div data-ng-show="modal.wmdPreview == true" id="wmd-preview-' + editorUniqueId + '" class="pagedownPreview wmd-panel wmd-preview">test div</div>' +
'</div>')(scope);
iElement.html(newElement);
var help = function () {
alert("There is no help");
}
var editor = new Markdown.Editor(converter, "-" + editorUniqueId, {
handler: help
});
var $wmdInput = iElement.find('#wmd-input-' + editorUniqueId);
var init = false;
editor.hooks.chain("onPreviewRefresh", function () {
var val = $wmdInput.val();
if (init && val !== ngModel.$modelValue) {
$timeout(function () {
scope.$apply(function () {
ngModel.$setViewValue(val);
ngModel.$render();
});
});
}
});
ngModel.$formatters.push(function (value) {
init = true;
$wmdInput.val(value);
// editor.refreshPreview();
return value;
});
editor.run();
}
}
});
Can someone explain to me what the following is doing:
scope: {
modal: '=modal'
},
and also the
)(scope);
Here is how I am calling this directive:
<textarea id="modal-data-text"
class="pagedown-admin wmd-preview-46"
data-modal="modal"
data-pagedown-admin
ng-model="home.modal.data.text"
ng-required="true"></textarea>
If anyone can see anything that may not work in 2 then I would much appreciate some help. In particular it seems that the following code returns null:
var $wmdInput = iElement.find('#wmd-input-' + editorUniqueId);
You dropped jQuery, so your code now relies on jQLite. Functions of element objects support less functionality when using jqLite. See the full details in the doc:
https://docs.angularjs.org/api/ng/function/angular.element
var $wmdInput = iElement.find('#wmd-input-' + editorUniqueId);
Under jqLite, the find function only support searching by tag names, ids will not work. You can use the following tricks from ( AngularJS: How to .find using jqLite? )
// find('#id')
angular.element(document.querySelector('#wmd-input-' + editorUniqueId))
$compile is a service that will compile a template and link it to a scope.
https://docs.angularjs.org/api/ng/service/$compile
scope: {
modal: '=modal'
}
allows you to define a isolated scope for the directive with some bindings to the scope in which the directive is declared. '=' is used for two-way data bindings. Other options are '# and &' for strings and functions.
https://docs.angularjs.org/guide/directive
I am trying to create a simple pagination directive with an isolated scope. For some reason when I manually change the value it gets a bit finnicky. Here is my problem:
When I page forward and backward, it works great. Awesome
When I enter a page into the field it works. Great
However, if I enter a page into the field and then try to go forward and backward, the ng-model seems to break after I enter a page into the field. I had it working when I did not isolate my scope but I am confused as to why it would break it. Here is my code:
HTML:
<paginate go-to-page="goToPage(page)" total-pages="results.hits.pages" total-hits="results.hits.total"></paginate>
Directive:
'use strict';
angular.module('facet.directives')
.directive('paginate', function(){
return {
restrict: 'E',
template: '<div class="pull-right" ng-if="(totalPages !== undefined) && (totalPages > 0)">'+
'<span class="left-caret hoverable" ng-click="changePage(current-1)" ng-show="current > 1"></span> Page'+
' <input type="number" ng-model="current" class="pagination-input" ng-keypress="enterPage($event)"/> of'+
' {{totalPages}} '+
'<span class="right-caret hoverable" ng-click="changePage(current+1)" ng-show="current < totalPages"></span>'+
'</div>',
scope: {
goToPage: '&',
totalPages: '=',
totalHits: '='
},
link: function(scope) {
scope.current = 1;
scope.changePage = function(page) {
scope.current = page;
window.scrollTo(0,0);
scope.goToPage({page:page});
};
scope.enterPage = function(event) {
if(event.keyCode == 13) {
scope.changePage(scope.current);
}
}
}
}
});
What am I doing wrong?
Beware of ng-if - it creates a new scope. If you change it to just ng-show, your example would work fine. If you do want to use ng-if, create a object to store the scope variable current. Maybe something like scope.state.current?
scope.state = {
current: 1
};
To avoid confusion like this, I always keep my bindings as something.something and never just something.
Edit: Good explanation here - http://egghead.io/lessons/angularjs-the-dot
Please always try to use model rather than using primitive types while using the ng-model because of the javascript's prototypical hierarchies.
angular.module('facet.directives').directive('paginate', function () {
return {
restrict: 'E',
replace: true,
template: '<div class="pull-right discovery-pagination" ng-if="(totalPages !== undefined) && (totalPages > 0)">' +
'<span class="left-caret hoverable" ng-click="changePage(current-1)" ng-show="current > 1"></span> Page' +
' <input type="number" ng-model="current.paging" class="pagination-input" ng-keypress="enterPage($event)"/> of' +
' {{totalPages}} ' +
'<span class="right-caret hoverable" ng-click="changePage(current+1)" ng-show="current < totalPages"></span>' +
'</div>',
scope: {
goToPage: '&',
totalPages: '=',
totalHits: '='
},
link: function(scope) {
scope.current = {paging:1};
scope.changePage = function(page) {
scope.current.paging = page;
window.scrollTo(0, 0);
scope.goToPage({ page: page });
};
scope.enterPage = function(event) {
if (event.keyCode == 13) {
scope.changePage(scope.current.paging);
}
};
}
};
});
Hope this will solve your problem :)
For detail about this, please go through Understanding-Scopes
I got a custom component in which i set the html with an xtemplate something like this
Ext.apply(me, { html: mainTpl.apply() });
I want to add some textfields to my XTemplate too and i cant figure out how to do this.
new Ext.XTemplate('<div Class="TopHeaderUserInfo"><div id="TopHeaderLanguageDiv" ><div Class="ActiveLanguageFlag" lang="{[ this.getLanguage() ]}" ></div>' +
'<ul Class="LangSelectDropDown HeaderDropDown" style="position:absolute;"><li class="ListHeader">' + MR.locale.SelectLanguage + '</li>{[ this.renderLanguageItems() ]}</ul>' +
'</div>{[this.renderUserInfo()]}</div>',
{
...
...
...
renderUserInfo: function () {
return (MR.classes.CurrentUser.user == null ?
'<div Class="LogInOut" Id="TopHeaderLoginLogoutLink"><a Class="Login">' + MR.locale['Login'] :
'<span>Welcome, ' + MR.classes.CurrentUser.getUser().get('FullName') + '</span> <a Class="Logout">' + MR.locale['Logout']) + '</a>' +
'<ul Class="HeaderDropDown LoginDropDown" style="position:absolute;"><li class="ListHeader">Header</li>' +
// INSERT TEXTFIELD HERE
'</ul>' +
'</div>';
}
})
please help - i dont know how to continue. couldnt find a solution in the web.
If you need any further information, dont hesitate to ask!
Here is a workaround. Tested with ExtJS 4.2.2.
You have to add to the template some container with an ID or a class name (e.g. <li class="custom-text-field"></li>). Then add handler for render event of your custom component. The handler will automatically insert your text field right after the template is rendered.
Ext.define('MyComponent', {
extend: 'Ext.container.Container',
initComponent: function() {
var me = this,
// just create a textfield and do not add it to any component
text = Ext.create('Ext.form.field.Text');
var mainTpl = new Ext.XTemplate("<div>{[this.renderUserInfo()]}</div>", {
renderUserInfo: function() {
return '<ul>' +
'<li class="custom-text-field"></li>' +
'</ul>';
}
}
);
me.html = mainTpl.apply();
// postpone text field rendering
me.on('render', function() {
// render text field to the <li class=".custom-text-field"></li>
text.render(me.getEl().down('.custom-text-field'));
});
this.callParent();
}
});
Ext.getBody().setHTML('');
Ext.create('MyComponent', {
renderTo: Ext.getBody()
});