When using ng-repeat what is the best way to be able to edit content?
In my ideal situation the added birthday would be a hyperlink, when this is tapped it will show an edit form - just the same as the current add form with an update button.
Live Preview (Plunker)
HTML:
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="utf-8">
<title>Custom Plunker</title>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.3/angular.min.js"></script>
<script>
document.write('<base href="' + document.location + '" />');
</script>
<script src="app.js"></script>
<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.2.0/css/bootstrap-combined.min.css"
rel="stylesheet">
</head>
<body ng-app="birthdayToDo" ng-controller="main">
<div id="wrap">
<!-- Begin page content -->
<div class="container">
<div class="page-header">
<h1>Birthday Reminders</h1>
</div>
<ul ng-repeat="bday in bdays">
<li>{{bday.name}} | {{bday.date}}</li>
</ul>
<form ng-show="visible" ng-submit="newBirthday()">
<label>Name:</label>
<input type="text" ng-model="bdayname" placeholder="Name" ng-required/>
<label>Date:</label>
<input type="date" ng-model="bdaydate" placeholder="Date" ng-required/>
<br/>
<button class="btn" type="submit">Save</button>
</form>
</div>
<div id="push"></div>
</div>
<div id="footer">
<div class="container">
<a class="btn" ng-click="visible = true"><i class="icon-plus"></i>Add</a>
</div>
</div>
</body>
App.js:
var app = angular.module('birthdayToDo', []);
app.controller('main', function($scope){
// Start as not visible but when button is tapped it will show as true
$scope.visible = false;
// Create the array to hold the list of Birthdays
$scope.bdays = [];
// Create the function to push the data into the "bdays" array
$scope.newBirthday = function(){
$scope.bdays.push({name:$scope.bdayname, date:$scope.bdaydate});
$scope.bdayname = '';
$scope.bdaydate = '';
};
});
You should put the form inside each node and use ng-show and ng-hide to enable and disable editing, respectively. Something like this:
<li>
<span ng-hide="editing" ng-click="editing = true">{{bday.name}} | {{bday.date}}</span>
<form ng-show="editing" ng-submit="editing = false">
<label>Name:</label>
<input type="text" ng-model="bday.name" placeholder="Name" ng-required/>
<label>Date:</label>
<input type="date" ng-model="bday.date" placeholder="Date" ng-required/>
<br/>
<button class="btn" type="submit">Save</button>
</form>
</li>
The key points here are:
I've changed controls ng-model to the local scope
Added ng-show to form so we can show it while editing
Added a span with a ng-hide to hide the content while editing
Added a ng-click, that could be in any other element, that toggles editing to true
Changed ng-submit to toggle editing to false
Here is your updated Plunker.
I was looking for a inline editing solution and I found a plunker that seemed promising, but it didn't work for me out of the box. After some tinkering with the code I got it working. Kudos to the person who made the initial effort to code this piece.
The example is available here http://plnkr.co/edit/EsW7mV?p=preview
Here goes the code:
app.controller('MainCtrl', function($scope) {
$scope.updateTodo = function(indx) {
console.log(indx);
};
$scope.cancelEdit = function(value) {
console.log('Canceled editing', value);
};
$scope.todos = [
{id:123, title: 'Lord of the things'},
{id:321, title: 'Hoovering heights'},
{id:231, title: 'Watership brown'}
];
});
// On esc event
app.directive('onEsc', function() {
return function(scope, elm, attr) {
elm.bind('keydown', function(e) {
if (e.keyCode === 27) {
scope.$apply(attr.onEsc);
}
});
};
});
// On enter event
app.directive('onEnter', function() {
return function(scope, elm, attr) {
elm.bind('keypress', function(e) {
if (e.keyCode === 13) {
scope.$apply(attr.onEnter);
}
});
};
});
// Inline edit directive
app.directive('inlineEdit', function($timeout) {
return {
scope: {
model: '=inlineEdit',
handleSave: '&onSave',
handleCancel: '&onCancel'
},
link: function(scope, elm, attr) {
var previousValue;
scope.edit = function() {
scope.editMode = true;
previousValue = scope.model;
$timeout(function() {
elm.find('input')[0].focus();
}, 0, false);
};
scope.save = function() {
scope.editMode = false;
scope.handleSave({value: scope.model});
};
scope.cancel = function() {
scope.editMode = false;
scope.model = previousValue;
scope.handleCancel({value: scope.model});
};
},
templateUrl: 'inline-edit.html'
};
});
Directive template:
<div>
<input type="text" on-enter="save()" on-esc="cancel()" ng-model="model" ng-show="editMode">
<button ng-click="cancel()" ng-show="editMode">cancel</button>
<button ng-click="save()" ng-show="editMode">save</button>
<span ng-mouseenter="showEdit = true" ng-mouseleave="showEdit = false">
<span ng-hide="editMode" ng-click="edit()">{{model}}</span>
<a ng-show="showEdit" ng-click="edit()">edit</a>
</span>
</div>
To use it just add water:
<div ng-repeat="todo in todos"
inline-edit="todo.title"
on-save="updateTodo($index)"
on-cancel="cancelEdit(todo.title)"></div>
UPDATE:
Another option is to use the readymade Xeditable for AngularJS:
http://vitalets.github.io/angular-xeditable/
I've modified your plunker to get it working via angular-xeditable:
http://plnkr.co/edit/xUDrOS?p=preview
It is common solution for inline editing - you creale hyperlinks with editable-text directive
that toggles into <input type="text"> tag:
<a href="#" editable-text="bday.name" ng-click="myform.$show()" e-placeholder="Name">
{{bday.name || 'empty'}}
</a>
For date I used editable-date directive that toggles into html5 <input type="date">.
Since this is a common piece of functionality it's a good idea to write a directive for this. In fact, someone already did that and open sourced it. I used editablespan library in one of my projects and it worked perfectly, highly recommended.
Related
In order to disable the auto complete of chrome in forms, i want to create angular directive which add dummy input element
my code is
`angular.module("noc.components").directive('customInput', function ($compile) {
"use strict";
return {
restrict:'E',
template:'<input>',
replace:true,
link: function(scope, elem) {
//scope.type = attrs.type || 'email';
var el = angular.element('<input name={{type}} style="display: none">');
//var type = elem[0].name;
$compile(el)(scope);
elem.parent().append(el);
}
};
});`
and the html is
<div class="form-group">
<label for="setupWizardUserStepEmail">Email Address</label>
<custom-input class="form-control"
id="setupWizardUserStepEmail"
ng-model="setupRequest.userRequest.email"
name="email"
ng-required="true"
noc-validation="email"
autocomplete="off"
autofocus="true">
</div>
however the injected html isnt in the parent - i want it to be above the directive
how can i do it?
I was able to solve my own problem in the end:
I changed it to elem.parent().prepend(el);
I already apologize for asking, just beginning with angularjs (and client-side dev) and I am trying to do something pretty simple which does not work.
I have some HTML that I want to generate from a variable. This html contains a ng-model directive that I want to bind with some data. Basically if I put directly the html inside the html file, data binding works fine:
HTML
<form role="form" ng-submit="search()" ng-controller="AppController">
<div class="form-group">
<label for="search__origin">Departure</label>
<input type="text" class="form-control" id="search__origin" ng-model="routes[0].origin" /> {{routes[0].origin}}
</div>
</form>
Anjularjs
var app = angular.module('my-app', [], function() {})
app.controller('AppController', function($scope) {
$scope.routes = {}
var route_format = {
origin: 'SG',
departure_date: new Date(),
return_date: new Date()
};
$scope.routes['0'] = route
})
Fiddle here
But if I put my HTML (containing a "ng-model" directive) into a $scope variable, then use "ng-bind-html" directive to generate the html directly into the html template, then data binding does not work:
HTML
<form role="form" ng-submit="search()" ng-controller="AppController">
<div class="form-group">
<label for="search__origin">Departure</label>
<div ng-bind-html=myhtml class="form-group"></div>
{{routes[0].origin}}
</div>
</form>
angularjs
var app = angular.module('my-app', [], function() {})
app.controller('AppController', function($scope,$sce) {
$scope.routes = {}
var route_format = {
origin: 'SG',
departure_date: new Date(),
return_date: new Date()
};
$scope.routes['0'] = route_format;
var html = '<input type="text" class="form-control" id="search__origin" ng-model="routes[0].origin"/>'
var getTrustedHtml = function(unsafe_html) {
var x = $sce.trustAsHtml(unsafe_html);
return x;
}
$scope.myhtml = getTrustedHtml(html);
})
Fiddle here
I am wondering here why it does not work, is it because I process in the wrong order and that when the html is generated on the page, the relation with the $scope variable cannot be done?
I would appreciate some help here :), thanks in advance!
Al
ng-bind-html will not compile directives for you.
you need either use $compile service and compile this fragment yourself that is ugly
or
restructure the code not to have directives inside ng-bind
Thanks STEVER for giving me the hint, appreciated!
I have solved my issue using the directive 'ng-include' and creating a html template. It also allows me to iterate ('ng-repeat') over this html template with dynamic 'ng-model's as below:
HTML
<form role="form" ng-submit="search()" ng-controller="AppController">
<div class="form-group">
<div ng-repeat="i in range(route_nb) track by $index">
<div ng-include="'test.html'"></div>
</div>
</div>
{{routes[0].origin}}
{{routes[1].origin}}
</form>
<script type="text/ng-template" id="test.html">
<div class="form-group">
<label for="search__origin">Departure</label>
<input type="text" class="form-control" id="search__origin" ng-model="routes[$index].origin" />
</div>
</script>
jsangular
var app = angular.module('my-app', [], function() {})
app.controller('AppController', function($scope) {
$scope.routes = {};
$scope.route_nb = 2;
$scope.routes['0'] = {
origin: 'SG',
departure_date: new Date(),
return_date: new Date()
};
$scope.routes['1'] = {
origin: 'LON',
departure_date: new Date(),
return_date: new Date()
};
$scope.range = function(n) {
return new Array(n);
};
})
Fiddle
However I am wondering whether it is really efficient in term of performances...
I have a form that contains 3 checkboxes: "Select All", "Option 1", and "Option 2".
<form id="selectionForm">
<input type="checkbox" ng-model="selectAll" >Select all
<br>
<input type="checkbox" ng-checked="selectAll" checked>Option 1
<br>
<input type="checkbox" ng-checked="selectAll">Option 2
</form>
On the initial page load I want only Option 1 to be checked. And then if the Select All checkbox gets checked it should automatically check Option 1 and Option 2 so all are selected.
The problem is on the initial page load the ng-checked="selectAll" gets evaluated which overrides my attempt to initially check only Option 1 (selectAll = false initially), so nothing is selected.
This seems like a simple problem to solve, but I can't figure out a solution... Thanks in advance for any insights or advice!
Another way to go about it is to use a model for the options, set default selection in the model and have your controller handle the logic of doing select all.
angular.module("app", []).controller("ctrl", function($scope){
$scope.options = [
{value:'Option1', selected:true},
{value:'Option2', selected:false}
];
$scope.toggleAll = function() {
var toggleStatus = !$scope.isAllSelected;
angular.forEach($scope.options, function(itm){ itm.selected = toggleStatus; });
}
$scope.optionToggled = function(){
$scope.isAllSelected = $scope.options.every(function(itm){ return itm.selected; })
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"> </script>
<div ng-app="app" ng-controller="ctrl">
<form id="selectionForm">
<input type="checkbox" ng-click="toggleAll()" ng-model="isAllSelected">Select all
<br>
<div ng-repeat = "option in options">
<input type="checkbox" ng-model="option.selected" ng-change="optionToggled()">{{option.value}}
</div>
</form>
{{options}}
</div>
Try this:
<form id="selectionForm">
<input type="checkbox" ng-model="selectAll" >Select all
<br>
<input type="checkbox" ng-checked="selectAll || option1" ng-init="option1=true" ng-model="option1">Option 1
<br>
<input type="checkbox" ng-checked="selectAll">Option 2
</form>
I like to use an ng-repeat for clarity on showing what you're selecting/un-selecting, basically you end up with a nice little object to base it all on, and adding to it is just easier.
Here's a Plunker
*Also notice how you can achieve allSelected? with a loop func and not a ton of html, and I'm sure this can be done with less spaghetti but it works *
app.controller('MainCtrl', function($scope) {
$scope.allSelected = false;
$scope.checkboxes = [{label: 'Option 1',checked: true}, {label: 'Option 2'}}}];
$scope.cbChecked = function(){
$scope.allSelected = true;
angular.forEach($scope.checkboxes, function(v, k) {
if(!v.checked){
$scope.allSelected = false;
}
});
}
$scope.toggleAll = function() {
var bool = true;
if ($scope.allSelected) {
bool = false;
}
angular.forEach($scope.checkboxes, function(v, k) {
v.checked = !bool;
$scope.allSelected = !bool;
});
}
});
I want to create function (in angularjs controller) which add particular class to the parent div of the empty input when clicking on the button. And remove that class when i type something in the input.
I have a working example here: http://jsfiddle.net/fUdLv/
HTML:
<div ng-app="authorization">
<div ng-controller="authCtrl as auth">
<div ng-class="{'error':auth.needEmail}">
<input type="text" ng-model="auth.email" ng-keypress="auth.clearEmail()">
</div>
<div ng-class="{'error':auth.needPassword}">
<input type="password" ng-model="auth.password" ng-keypress="auth.clearPassword()">
</div>
<div>
<button ng-click="auth.signin()">Sign in</button>
</div>
</div>
</div>
Javascript:
angular.module('authorization', [])
.controller('authCtrl', function() {
this.needEmail = false;
this.needPassword = false;
this.signin = function pay() {
if (!this.email){
this.needEmail = true;
}
if (!this.password){
this.needPassword = true;
}
};
this.clearEmail = function(){
this.needEmail = false;
}
this.clearPassword = function(){
this.needPassword = false;
}
});
But i'm sure it is very bad code, because i have a particular function for working with particular input. How can i generalize this function for working on every input (for example, if i would have three or four inputs it is not very smart solution to create function for each of them)
How about use <form> and FormController to control state?
You probably need to read this guide from official site.
I have a Twitter Bootstrap modal that I am displaying where I want to include a "Copy to Clipboard" button. I am attempting to use the ZeroClipboard component https://github.com/jonrohan/ZeroClipboard
Below is my sample code. Button "Copy1" is on a page directly and that works. Button "Copy2" is on the modal and that is not working. Internet Explorer appears to "lock up" when "Copy2" is pressed.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<link type="text/css" rel="stylesheet" href="/Content/bootstrap.min.css" />
<script src="/Scripts/jquery-1.9.1.min.js"></script>
<script src="/Scripts/ZeroClipboard.min.js"></script>
<script src="/Scripts/bootstrap.min.js"></script>
</head>
<body>
<div class="row">
<div class="span6">
<!-- Textbox and copy button pair #1 (not on modal) -->
<input type="text" id="Input1" />
<button class="btn" type="button" id="Copy1" data-clipboard-target="Input1">Copy Input #1</button>
</div>
<div class="span6">
<a class="btn" type="button" href="#Modal1" data-toggle="modal">Show Modal Dialog</a>
</div>
</div>
<div class="modal hide fade" role="dialog" id="Modal1">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3>Copy to Clipboard Modal</h3>
</div>
<div class="modal-body">
<p>
<!-- Textbox and copy button pair #2 (on modal) -->
<input type="text" id="Input2" />
<button class="btn" type="button" id="Copy2" data-clipboard-target="Input2">Copy Input #2</button>
</p>
</div>
<div class="modal-footer">
<p>
<button class="btn" type="button" data-dismiss="modal">Close</button>
</p>
</div>
</div>
<script type="text/javascript">
$(document).ready(function () {
ZeroClipboard.setDefaults({
moviePath: '/Scripts/ZeroClipboard.swf'
});
var clip1 = new ZeroClipboard($("#Copy1"));
var clip2 = new ZeroClipboard($("#Copy2"));
});
</script>
</body>
</html>
Can anyone offer some guidance on what is going wrong? I understand that Bootstrap takes the modal out of the DOM tree until displayed, but I'm not sure how to accommodate for that.
Edit: Corrected id of 2nd input to be "Input2" to match the button's target.
In addition, I have attempted the following javascript:
//var clip2 = new ZeroClipboard($("#Copy2"));
$("#Modal1").on('shown', function () {
var clip2 = new ZeroClipboard($("#Copy2"));
});
Also, it would appear that the issue is browser-specific.
The original code and my modified code locks up Internet Explorer 10. But Google Chrome is OK under both code attempts.
Your 2nd copy button is targeting a nonexistent ID:
<button class="btn" type="button" id="Copy2" data-clipboard-target="Input2">Copy Input #2</button>
Your markup is incorrect:
<input type="text" id="Text2" />
Should be:
<input type="text" id="Input2" />
When we use "ZeroClipboard" with "Bootstrap modal", it creates conflict in "IE".
We need to make changes in bootstrap's "js" file.
There is method defined for, “ Modal.prototype.enforceFocus “ element in “Bootstrap.js” .
Here, in “If” condition, we need to add following condition,
!$(e.target).closest('[id ^= "ZeroClipboardMovie_"]').length)
Resultant code will look as follows,
if (this.$element[0] !== e.target && !this.$element.has(e.target).length && !$(e.target).closest('[id ^= "ZeroClipboardMovie_"]').length) {
this.$element.focus()
}
where, [id ^= "ZeroClipboardMovie_"] is ZeroClipboard container element.
In above code, it is taking container of recent "ZeroClipboard". You can change it to, appropriate "zeroclipboard-container" as per the requirement.
You can also checkout this blog post for more detail- http://wisdmlabs.com/blog/ie-issue-zeroclipboard-bootstrap-modal/
if (!checkIsBelowIE8()) {
if (/MSIE|Trident/.test(window.navigator.userAgent)) {
(function ($) {
var zcClass = '.' + ZeroClipboard.config('containerClass');
var proto = $.fn.modal.Constructor.prototype;
proto.enforceFocus = function () {
$(document)
.off('focusin.bs.modal') /* Guard against infinite focus loop */
.on('focusin.bs.modal', $.proxy(function (e) {
if (this.$element[0] !== e.target &&
!this.$element.has(e.target).length &&
/* Adding this final condition check is the only real change */
!$(e.target).closest(zcClass).length
) {
this.$element.focus();
}
}, this));
};
})(window.jQuery);
}
var client = new ZeroClipboard($(".clipboard"));
client.on("copy", function (e) {
var clipboard = e.clipboardData;
clipboard.setData("text/plain", $.trim($(e.target).parent().prev().find("input").val()));
showMsg("success", "copy success");
});
}
function checkIsBelowIE8() {
try {
var b_name = navigator.appName;
var b_version = navigator.appVersion;
var version = b_version.split(";");
var trim_version = version[1].replace(/[ ]/g, "");
if (b_name == "Microsoft Internet Explorer") {
/*if IE6 or IE7*/
if (trim_version == "MSIE8.0" || trim_version == "MSIE7.0" || trim_version == "MSIE6.0") {
return true;
} else {
return false;
}
} else {
return false;
}
} catch (e) {
return false;
}
}
hope to help you