Saturday, March 15, 2014

Edit-in-place input field implementation with AngularJS

While working on a little side project with Rails and AngularJS, I needed an edit-in-place (or click-to-edit) functionality for text input fields.

Edit-in-place means that the text input field will be toggled between edit-mode and preview-mode, and when in edit-mode - you can persist the change to the server and continue working without page refresh.

This approach is becoming common in reactive web-apps and SPAs, while The "save" button for the whole form is gradually disappearing.

My implementation provides a directive, an HTML template, a controller (in the directive) and a service. With this configuration your controllers remain untouched, since the directive's controller is injected with a generic service and uses it directly. The service uses Restangular to talk to my Rails back-end.

So here's the Gist:

Yes, this code still needs some refactoring..
FoodBetterApp.factory('EditableInput', function(Restangular) {
return {
saveFieldValue: function(pluralResourceName, resourceId, fieldName, fieldValue) {
var postData = {};
postData[fieldName] = fieldValue;
return Restangular.one(pluralResourceName, resourceId).put(postData);
}
};
});
<div>
<textarea ng-show="isEditMode" ng-model="fieldValue" rows="2" class="form-control span3" autofocus>{{getPreviewText()}}</textarea>
<span ng-click="saveData()" ng-show="isEditMode" class="glyphicon glyphicon-ok clickable"></span>
<span ng-click="cancelEdit()" ng-show="isEditMode" class="glyphicon glyphicon-remove clickable"></span>
<div ng-hide="isEditMode" ng-click="switchToEditMode()">{{getPreviewText()}}</div>
</div>
FoodBetterApp.directive('editableInput', ['EditableInput', function(EditableInput) {
return {
restrict: 'E',
replace: true,
templateUrl: 'partials/editable-text/editable-text.html',
scope: {
resourceId: '@',
resourceNameSingle: '@',
resourceNamePlural: '@',
fieldName: '@',
fieldValue: '@',
lastFieldValue: '@fieldValue'
},
compile: function(element, attributes, transclude) {
return function(scope, element, attributes) {
scope.isEditMode = false;
scope.switchToEditMode = function() {
scope.isEditMode = true;
var fieldValueTextArea = $(element).find('textarea');
var textBoxValue = scope.lastFieldValue;
fieldValueTextArea.val('');
fieldValueTextArea.blur().focus();
fieldValueTextArea.val(textBoxValue);
};
scope.switchToPreviewMode = function() {
scope.isEditMode = false;
};
scope.saveData = function() {
EditableInput.saveFieldValue(scope.resourceNamePlural, scope.resourceId,
scope.fieldName, scope.fieldValue).then(function() {
scope.lastFieldValue = scope.fieldValue;
scope.switchToPreviewMode();
});
};
scope.cancelEdit = function() {
scope.fieldValue = scope.lastFieldValue;
scope.switchToPreviewMode();
};
scope.getPreviewText = function() {
return (scope.fieldValue === '' ? 'click to edit' : scope.fieldValue);
};
};
}
};
}]);
<editable-input resource-id="{{recipe.id}}" resource-name-single="recipe"
resource-name-plural="recipes" field-name="notes" field-value="{{recipe.notes}}"></editable-input>
view raw view.html hosted with ❤ by GitHub