I have created a custom node in node-red
<script type="text/javascript">
RED.nodes.registerType('project', {
category: 'My Category',
color: 'rgb(192, 237, 192)',
defaults: {
name: { value: "", required:true },
project: { value: "", required:true }
},
inputs: 0,
outputs: 1,
onpaletteadd: function (index) {
var node = this;
var sessionStorageData = sessionStorage.getItem(node.z);
if (sessionStorageData && (JSON.parse(sessionStorageData)).id != node.id) {
alert("Flow cannot have more then one Project node!!!")
}
if (!sessionStorageData) {
sessionStorageData = { id: node.id }
} else {
sessionStorageData = JSON.parse(sessionStorageData);
}
sessionStorageData.project = node.project;
sessionStorage.setItem(node.z, JSON.stringify(sessionStorageData));
},
oneditprepare: function (index) {
var node = this;
$.ajax({
type: "GET",
url: "../getExternalData?path=get",
dataType: "json",
success: function (data1) {
this.preload = true;
var appenddata1 = "";
$.each(data1, function (key, val) {
appenddata1 += "<option value = '" + key + "'>" + val + " </option>";
});
$("#node-input-project").append(appenddata1);
$("#node-input-project").val(node.project);
}
});
},
oneditsave: function (index) {
var node = this;
var sessionStorageData = sessionStorage.getItem(node.z);
if (!sessionStorageData) {
sessionStorageData = { id: node.id }
} else {
sessionStorageData = JSON.parse(sessionStorageData);
}
sessionStorageData.project = $("#node-input-project").val();
sessionStorage.setItem(node.z, JSON.stringify(sessionStorageData));
},
icon: "cog.png",
label: function () {
return this.name || "Project";
}
});
</script>
<script type="text/x-red" data-template-name="project">
<div class="form-row">
<label for="node-input-name"><i class="icon-tag"></i>Workflow Name</label>
<input type="text" id="node-input-name" placeholder="Workflow Name"/>
</div>
<div class="form-row">
<label for="node-input-project"><i class="icon-tag"></i> Project</label>
<select type="text" id="node-input-project">
<option value=" ">Please select a Project</option>
</select>
</div>
</script>
to my surprise required is only making the text box red and is not stopping the popup close on click of Done button, i tried return false in oneditsave but that doesn't help either.
As described in the Node-RED docs on creating nodes, you can add a validate function to the defaults section of the node's html file.
There are 2 built in validators,
RED.validators.number()
RED.validators.regex(re)
But you can attach your own function as well:
defaults: {
minimumLength: { value:0, validate:RED.validators.number() },
lowerCaseOnly: {value:"", validate:RED.validators.regex(/[a-z]+/) },
custom: { value:"", validate:function(v) {
var minimumLength=$("#node-input-minimumLength").length?$("#node-input-minimumLength").val():this.minimumLength;
return v.length > minimumLength
} }
},
But even after using either these or the required: true the user will still be able to hit the Done button. A warning will be shown to the user when they try to deploy a flow that contains nodes that have missing or invalid field values but there is no way to stop a user deploying a flow with bad input data.
Related
I'm using jquery-ui to add autocomplete to an input field. I have essentially a two stage autocomplete that I'm trying to set up. Typing M will display an autocomplete of all options, selecting an option will enter that into the input EX: "machineName=", depending upon the first option selected, I then want to load a second autocomplete in the same field to show the values for that filter.
It works using static data, however the second autocomplete, is using API data so I have an AJAX call at the beginning of my script with a .then to chain together the creation of the autocomplete after the API has been hit, typing anything after machineName= results in nothing being displayed, however logging the value of the array I can see all the values in it.
var occupations = [{
value: "machineName=",
label: "machineName"
}, {
value: "ipAddress=",
label: "ipAddress"
},];
let machineNameAC = []
let switchTerm= [];
$.ajax({
url: '/api/data',
contentType: 'application/json',
dataType: 'json',
type: 'GET',
success: function(response){
console.log(response)
response.result.forEach((res) => {
machineNameAC.push(res.machineName)
console.log(machineNameAC)
})
}
}).then(() => {
$(function() {
function split(val) {
return val.split('=');
}
function extractLast(term) {
return split(term).pop();
}
$("#occupation").on("keydown", function(event) {
if (event.keyCode === $.ui.keyCode.TAB &&
$(this).autocomplete("instance").menu.active) {
event.preventDefault();
}
}).autocomplete({
minLength: 0,
source: function(request, response) {
var term = extractLast(request.term);
var results = [];
if (request.term.indexOf("=") > 0) {
var regE = /([^=]*)$/
if (request.term.endsWith('=')) {
console.log('term ',request.term)
switch (request.term){
case 'machineName=':
machineNameAC.forEach((machine)=>{
switchTerm.push(machine)
})
break;
}
}
console.log(switchTerm)
console.log(request.term)
if (parseInt(term) > 0) {
$.each(machineNameAC, function(k, v) {
console.log(k, v)
results.push(term + "" + v);
});
}
} else {
results = $.ui.autocomplete.filter(
occupations, request.term);
}
response(results);
},
focus: function() {
// prevent value inserted on focus
return false;
},
select: function(event, ui) {
var terms = split(this.value);
terms[0] = terms[0] + "=";
// remove the current input
terms.pop();
// add the selected item
terms.push(ui.item.value);
// add placeholder to get the comma-and-space at the end
terms.push("");
this.value = terms.join("");
return false;
}
});
});
})
Based on your code, your first stage determines if the user will look up a machine name versus an IP Address. Example:
machineName=descartes or ipAddress=192.168.1.112
The example data you provided was:
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
This data does not have a good relationship with the search in your initial code. I am assuming you have multiple items and you want to review the term against the title, yet this is not seen in your example. This is likely just sample data and does not represent your actual data. I must assume your data is more like:
[{
userId: 1001,
userName: "John Smith",
machineName: "jsmith-1",
ipAddress: "192.168.1.112",
siteLocation: "Shipping Bay 1"
}, {
userId: 1002,
userName: "Bettie Page",
machineName: "bpage-1",
ipAddress: "192.168.1.169",
siteLocation: "Champagne Room"
}];
If this is the case, you can forgo the prefix and make it a single state lookup that examines the term and identifies a machine name (words) versus an IP address (numbers and dots) and supplies the correct data. It's your choice to have it be two stage or single stage. You will need a filter function for both types, regardless.
Something like this:
function filterMachine(term, myData) {
var results = [];
$.each(myData, function(key, val) {
if (val.machineName.toLowerCase().indexOf(term.toLowerCase()) != -1) {
results.push($.extend(val, {
label: val.machineName,
value: val.machineName
});
}
});
return results;
}
function filterIp(term, myData) {
var results = [];
$.each(myData, function(key, val) {
if (val.ipAddress.indexOf(term) == 0) {
results.push($.eaxtend(val, {
label: val.ipAddress,
value: val.ipAddress
}));
}
});
return results;
}
It's not clear from your post what you are then doing with this data once a user Selects the item. Maybe filtering a table or updating a form. I do suggest using minLength option, something like 2. In this example, they might enter 192 or des and this should be enough to logically determin what they are seeking.
Single Stage Example: https://jsfiddle.net/Twisty/60a1eLwz/32/
Your code will be a bit different as I am using the Echo feature of Fiddle, it does not use GET. Instead you POST data and it echos it back to the request.
JavaScript
$(() => {
var sampleData = [{
userId: 1001,
userName: "John Smith",
machineName: "jsmith-1",
ipAddress: "192.168.1.112",
siteLocation: "Shipping Bay 1"
}, {
userId: 1002,
userName: "Bettie Page",
machineName: "bpage-1",
ipAddress: "192.168.1.169",
siteLocation: "Champagne Room"
}];
function filterMachine(term, myData) {
var results = [];
$.each(myData, function(key, val) {
if (val.machineName.toLowerCase().indexOf(term.toLowerCase()) != -1) {
results.push($.extend(val, {
label: val.machineName,
value: val.machineName
}));
}
});
return results;
}
var apiData;
function filterIp(term, myData) {
var results = [];
$.each(myData, function(key, val) {
if (val.ipAddress.indexOf(term) == 0) {
results.push($.extend(val, {
label: val.ipAddress,
value: val.ipAddress
}));
}
});
return results;
}
$.ajax({
url: '/echo/json',
dataType: 'json',
type: 'POST',
data: {
json: JSON.stringify(sampleData)
},
success: function(response) {
apiData = response;
console.log('API Data ', apiData);
$("#inputField").on("keydown", function(event) {
if (event.keyCode === $.ui.keyCode.TAB &&
$(this).autocomplete("instance").menu.active) {
event.preventDefault();
}
}).autocomplete({
minLength: 2,
source: function(request, response) {
if (isNaN(request.term.replace(".", ""))) {
response(filterMachine(request.term, apiData));
} else {
response(filterIp(request.term, apiData));
}
},
focus: function() {
return false;
},
select: function(event, ui) {
$("#results").empty();
$.each(ui.item, function(key, value) {
$("#results").append(key + ": " + value + "<br />");
})
return false;
}
});
}
});
});
This is more similar to your code, where it collects all the data up front and it's in a variable. You could also just call the JSON content in the source upon each request. The only benefit is if there are frequent changes to the data, you will catch more of them. If you pull all the data when the page loads, and the user sits on the page for 1 or 2 minutes, the data source could potentially be updated and the User will not get that new data.
In my opinion, this is a more friendly user interface, the User enters what they are looking for without having to search twice. They get suggests and pull up the result. With the Two Stage, they have to make an initial selection and then search. If you still want two stage, I suspect you can see where you would inject your prefix code again and then on the second stage pass the term to the correct function and append the specific detail you need as the result.
Two Stage Example: https://jsfiddle.net/Twisty/60a1eLwz/46/
JavaScript
$(() => {
var sampleData = [{
userId: 1001,
userName: "John Smith",
machineName: "jsmith-1",
ipAddress: "192.168.1.112",
siteLocation: "Shipping Bay 1"
}, {
userId: 1002,
userName: "Bettie Page",
machineName: "bpage-1",
ipAddress: "192.168.1.169",
siteLocation: "Champagne Room"
}];
function filterMachine(term, myData) {
var results = [];
$.each(myData, function(key, val) {
if (val.machineName.toLowerCase().indexOf(term.toLowerCase()) != -1) {
results.push($.extend(val, {
label: val.machineName,
value: val.machineName
}));
}
});
return results;
}
function filterIp(term, myData) {
var results = [];
$.each(myData, function(key, val) {
if (val.ipAddress.indexOf(term) == 0) {
results.push($.extend(val, {
label: val.ipAddress,
value: val.ipAddress
}));
}
});
return results;
}
$("#inputField").on("keydown", function(event) {
if (event.keyCode === $.ui.keyCode.TAB &&
$(this).autocomplete("instance").menu.active) {
event.preventDefault();
}
}).autocomplete({
minLength: 0,
source: function(request, response) {
if (request.term.length <= 2) {
response([{
label: "machineName",
value: "machineName="
}, {
label: "ipAddress",
value: "ipAddress="
}]);
} else {
var terms = request.term.split("=");
console.log(request.term, terms);
$.ajax({
url: '/echo/json',
dataType: 'json',
type: 'POST',
data: {
json: JSON.stringify(sampleData)
},
success: function(data) {
if (terms[0] == "machineName") {
response(filterMachine(terms[1], data));
} else {
response(filterIp(terms[1], data));
}
}
});
}
},
focus: function() {
return false;
},
select: function(event, ui) {
var terms = this.value.split("=");
terms[0] = terms[0] + "=";
// remove the current input
terms.pop();
// add the selected item
terms.push(ui.item.value);
// add placeholder to get the comma-and-space at the end
terms.push("");
this.value = terms.join("");
return false;
}
});
});
I have the same problem with this one Why ui grid value drop-down box will be assigned in the event fires before beginCellEdit in angular
And have little bit difference. After I updated the editDropdownOptionArray it still keep the old value, It's only have the new value in the next click.
Hope everyone can help me with this one.
Thank you.
Here is my code snippet:
The html of the dropdown:
<div>
<form name="inputForm">
<select ng-class="'colt' + col.uid"
ui-grid-edit-dropdown
ng-model="MODEL_COL_FIELD"
ng-options="field CUSTOM_FILTERS for field in editDropdownOptionsArray"></select>
</form>
</div>
The controller code:
$scope.menuColumns = [
{ displayName: 'Menu', field: 'name', enableCellEdit: false },
{
displayName: 'Access Level', field: 'accessLevelName',
editableCellTemplate: 'Scripts/application/role/directive/dropdown-menu-assignment.html',
editDropdownValueLabel: 'Access Level', editDropdownOptionsArray: userMgtConstant.menuAccessLevel
}
];
$scope.menuOptions = {
data: [],
onRegisterApi: function (gridApi) {
gridApi.edit.on.beginCellEdit($scope, function (rowEntity, colDef, event) {
if (rowEntity.parent === true) {
colDef.editDropdownOptionsArray = $scope.levelList;
} else {
colDef.editDropdownOptionsArray = $scope.childLevelList;
}
});
gridApi.edit.on.afterCellEdit($scope, function (rowEntity, colDef, newValue, oldValue) {
if (rowEntity.parent !== true) {
if(rowEntity.name === 'Revenue Bench'){
var accessLevel = commonUtils.getIdFromName(rowEntity.accessLevelName, userMgtConstant.menuAccessLevel);
if(accessLevel > 1){
$scope.isShowFunctionAssignment = false;
} else if(rowEntity.functionAssignments.length !== 0) {
$scope.isShowFunctionAssignment = true;
}
}
} else {
// udpate child dropdown list menu
var index = _(userMgtConstant.menuAccessLevel).indexOf(newValue);
$scope.childLevelList = $scope.levelList.filter(function (item, i) {
return i >= index;
});
if($scope.childLevelList.length > 2){
parentSwitch = true;
}
if($scope.childLevelList.length < 3 && parentSwitch){
colDef.editDropdownOptionsArray = $scope.childLevelList;
parentSwitch = false;
}
// update all child value
_($scope.menuOptions.data).each(function (dataItem) {
if (dataItem.parent !== true) { // prevent infinite loop
dataItem.accessLevelName = newValue;
}
});
}
});
}
};
Here is the usage of the grid:
<inline-edit-grid options="menuOptions" columns="menuColumns"></inline-edit-grid>
You should look into the usage of editDropdownRowEntityOptionsArrayPath instead of editDropdownOptionsArray
From the docs :
editDropdownRowEntityOptionsArrayPath can be used as an alternative to
editDropdownOptionsArray when the contents of the dropdown depend on
the entity backing the row.
Here is a link to tutorial
I'm using select2 JQuery plugin to implement an autocomplete input-like element backed by a json endpoint, in this select2 input, I want the user can also create new objects into the same element by just giving them names or using the available on the endpoint.
My problem is that I need to access the processed 'data' mapping generated on 'processResults' key outside its function, actually inside the createTag function, I'm not sure if I need to use some JQuery method to access the result or control it using some global variable.
Here is my code:
HTML:
<p>
<select class="js-example-tags form-control" multiple="multiple"></select>
</p>
JS:
var $tags = $(".js-example-tags");
var responsejson;
$.fn.select2.amd.require(['select2/compat/matcher'], function(oldMatcher) {
$tags.select2({
ajax: {
data: function(params) {
var unicode = "\uf8ff";
var startAt = '"' + params.term + '"';
var endAt = '"' + params.term + unicode + '"';
var query = {
orderBy: "\"lowerCaseName\"",
startAt: startAt.toLowerCase(),
endAt: endAt.toLowerCase(),
print: "\"pretty\""
};
// Query paramters will be ?search=[term]&page=[page]
return query;
},
url: 'https://someappname.firebaseio.com/substancies.json?',
processResults: function(data, key) {
return {
results: $.map(data, function(obj, key) {
responsejson = {
id: key,
lower: obj.lowerCaseName,
text: obj.commonName
};
return responsejson;
})
};
}
},
tags: true,
createTag: function(params) {
if (responsejson !== undefined) {
console.log("Oppa");
}
var term = $.trim(params.term);
if (term === "") {
return null;
}
var optionsMatch = false;
var arrValue = $(".js-example-tags").select2('data');
for (var i = 0; i < arrValue.length; i++) {
var var1 = arrValue[i].lower;
var var2 = term.toLowerCase();
if (term.toLowerCase() === arrValue[i].lower) {
optionsMatch = true;
break;
}
}
if (optionsMatch) {
return null;
}
return {
id: -1,
text: term
};
},
minimumInputLength: 3,
tokenSeparators: [','],
casesensitive: false
});
});
I'm starting to learn and azure phonejs.
Todo list get through a standard example:
$(function() {
var client = new WindowsAzure.MobileServiceClient('https://zaburrito.azure-mobile.net/', 'key');
var todoItemTable = client.getTable('todoitem');
// Read current data and rebuild UI.
// If you plan to generate complex UIs like this, consider using a JavaScript templating library.
function refreshTodoItems() {
var query = todoItemTable.where({ complete: false });
query.read().then(function(todoItems) {
var listItems = $.map(todoItems, function(item) {
return $('<li>')
.attr('data-todoitem-id', item.id)
.append($('<button class="item-delete">Delete</button>'))
.append($('<input type="checkbox" class="item-complete">').prop('checked', item.complete))
.append($('<div>').append($('<input class="item-text">').val(item.text)));
});
$('#todo-items').empty().append(listItems).toggle(listItems.length > 0);
$('#summary').html('<strong>' + todoItems.length + '</strong> item(s)');
}, handleError);
}
function handleError(error) {
var text = error + (error.request ? ' - ' + error.request.status : '');
$('#errorlog').append($('<li>').text(text));
}
function getTodoItemId(formElement) {
return $(formElement).closest('li').attr('data-todoitem-id');
}
// Handle insert
$('#add-item').submit(function(evt) {
var textbox = $('#new-item-text'),
itemText = textbox.val();
if (itemText !== '') {
todoItemTable.insert({ text: itemText, complete: false }).then(refreshTodoItems, handleError);
}
textbox.val('').focus();
evt.preventDefault();
});
// Handle update
$(document.body).on('change', '.item-text', function() {
var newText = $(this).val();
todoItemTable.update({ id: getTodoItemId(this), text: newText }).then(null, handleError);
});
$(document.body).on('change', '.item-complete', function() {
var isComplete = $(this).prop('checked');
todoItemTable.update({ id: getTodoItemId(this), complete: isComplete }).then(refreshTodoItems, handleError);
});
// Handle delete
$(document.body).on('click', '.item-delete', function () {
todoItemTable.del({ id: getTodoItemId(this) }).then(refreshTodoItems, handleError);
});
// On initial load, start by fetching the current data
refreshTodoItems();
});
and it works!
Changed for the use of phonejs and the program stops working, even mistakes does not issue!
This my View:
<div data-options="dxView : { name: 'home', title: 'Home' } " >
<div class="home-view" data-options="dxContent : { targetPlaceholder: 'content' } " >
<button data-bind="click: incrementClickCounter">Click me</button>
<span data-bind="text: listData"></span>
<div data-bind="dxList:{
dataSource: listData,
itemTemplate:'toDoItemTemplate'}">
<div data-options="dxTemplate:{ name:'toDoItemTemplate' }">
<div style="float:left; width:100%;">
<h1 data-bind="text: name"></h1>
</div>
</div>
</div>
</div>
This my ViewModel:
Application1.home = function (params) {
var client = new WindowsAzure.MobileServiceClient('https://zaburrito.azure-mobile.net/', 'key');
var todoItemTable = client.getTable('todoitem');
var toDoArray = ko.observableArray([
{ name: "111", type: "111" },
{ name: "222", type: "222" }]);
var query = todoItemTable.where({ complete: false });
query.read().then(function (todoItems) {
for (var i = 0; i < todoItems.length; i++) {
toDoArray.push({ name: todoItems[i].text, type: "NEW!" });
}
});
var viewModel = {
listData: toDoArray,
incrementClickCounter: function () {
todoItemTable = client.getTable('todoitem');
toDoArray.push({ name: "Zippy", type: "Unknown" });
}
};
return viewModel;
};
I can easily add items to the list of programs, but from the server list does not come:-(
I am driven to exhaustion and can not solve the problem for 3 days, which is critical for me!
Specify where my mistake! Thank U!
I suggest you use a DevExpress.data.DataSource and a DevExpress.data.CustomStore instead of ko.observableArray.
Application1.home = function (params) {
var client = new WindowsAzure.MobileServiceClient('https://zaburrito.azure-mobile.net/', 'key');
var todoItemTable = client.getTable('todoitem');
var toDoArray = [];
var store = new DevExpress.data.CustomStore({
load: function(loadOptions) {
var d = $.Deferred();
if(toDoArray.length) {
d.resolve(toDoArray);
} else {
todoItemTable
.where({ complete: false })
.read()
.then(function(todoItems) {
for (var i = 0; i < todoItems.length; i++) {
toDoArray.push({ name: todoItems[i].text, type: "NEW!" });
}
d.resolve(toDoArray);
});
}
return d.promise();
},
insert: function(values) {
return toDoArray.push(values) - 1;
},
remove: function(key) {
if (!(key in toDoArray))
throw Error("Unknown key");
toDoArray.splice(key, 1);
},
update: function(key, values) {
if (!(key in toDoArray))
throw Error("Unknown key");
toDoArray[key] = $.extend(true, toDoArray[key], values);
}
});
var source = new DevExpress.data.DataSource(store);
// older version
store.modified.add(function() { source.load(); });
// starting from 14.2:
// store.on("modified", function() { source.load(); });
var viewModel = {
listData: source,
incrementClickCounter: function () {
store.insert({ name: "Zippy", type: "Unknown" });
}
};
return viewModel;
}
You can read more about it here and here.
I am looking to run the following controller but im having trouble with scope.
I have a service that calls two functions that retrieve meta data to populate scope variables.
The issue is that using the service to call back the data interferes with other actions happening in the code. I have a directive on a tag that shows/hides an error on the span element once the rule is validated. This is now not functioning correctly. I run the code without asynchronous functions then everything works correctly.
My Plunker code is here
and the plunker of the desired behaviour is here
Plunker working example without dynamic data loading
<form class="form-horizontal">
<div class="control-group" ng-repeat="field in viewModel.Fields">
<label class="control-label">{{field.label}}</label>
<div class="controls">
<input type="text" id="{{field.Name}}" ng-model="field.data" validator="viewModel.validator" ruleSetName="{{field.ruleSet}}"/>
<span validation-Message-For="{{field.Name}}"></span>
</div>
</div>
<button ng-click="save()">Submit</button>
</form>
How do I get all bindings to update so everything is sync and loaded correctly?
angular.module('dataApp', ['servicesModule', 'directivesModule'])
.controller('dataCtrl', ['$scope', 'ProcessService', 'ValidationRuleFactory', 'Validator',
function($scope, ValidationRuleFactory, Validator, ProcessService) {
$scope.viewModel = {};
var FormFields = {};
// we would get this from the meta api
ProcessService.getProcessMetaData().then(function(data) {
alert("here");
FormFields = {
Name: "Course",
Fields: [{
type: "text",
Name: "name",
label: "Name",
data: "",
required: true,
ruleSet: "personFirstNameRules"
}, {
type: "text",
Name: "description",
label: "Description",
data: "",
required: true,
ruleSet: "personEmailRules"
}]
};
$scope.viewModel.Fields = FormFields;
ProcessService.getProcessRuleData().then(function(data) {
var genericErrorMessages = {
required: 'Required',
minlength: 'value length must be at least %s characters',
maxlength: 'value length must be less than %s characters'
};
var rules = new ValidationRuleFactory(genericErrorMessages);
$scope.viewModel.validationRules = {
personFirstNameRules: [rules.isRequired(), rules.minLength(3)],
personEmailRules: [rules.isRequired(), rules.minLength(3), rules.maxLength(7)]
};
$scope.viewModel.validator = new Validator($scope.viewModel.validationRules);
});
});
var getRuleSetValuesMap = function() {
return {
personFirstNameRules: $scope.viewModel.Fields[0].data,
personEmailRules: $scope.viewModel.Fields[1].data
};
};
$scope.save = function() {
$scope.viewModel.validator.validateAllRules(getRuleSetValuesMap());
if ($scope.viewModel.validator.hasErrors()) {
$scope.viewModel.validator.triggerValidationChanged();
return;
} else {
alert('person saved in!');
}
};
}
]);
The validation message directive is here
(function(angular, $) {
angular.module('directivesModule')
.directive('validationMessageFor', [function() {
return {
restrict: 'A',
scope: {eID: '#val'},
link: function(scope, element, attributes) {
//var errorElementId = attributes.validationMessageFor;
attributes.$observe('validationMessageFor', function(value) {
errorElementId = value;
//alert("called");
if (!errorElementId) {
return;
}
var areCustomErrorsWatched = false;
var watchRuleChange = function(validationInfo, rule) {
scope.$watch(function() {
return validationInfo.validator.ruleSetHasErrors(validationInfo.ruleSetName, rule.errorCode);
}, showErrorInfoIfNeeded);
};
var watchCustomErrors = function(validationInfo) {
if (!areCustomErrorsWatched && validationInfo && validationInfo.validator) {
areCustomErrorsWatched = true;
var validator = validationInfo.validator;
var rules = validator.validationRules[validationInfo.ruleSetName];
for (var i = 0; i < rules.length; i++) {
watchRuleChange(validationInfo, rules[i]);
}
}
};
// get element for which we are showing error information by id
var errorElement = $("#" + errorElementId);
var errorElementController = angular.element(errorElement).controller('ngModel');
var validatorsController = angular.element(errorElement).controller('validator');
var getValidationInfo = function() {
return validatorsController && validatorsController.validationInfoIsDefined() ? validatorsController.validationInfo : null;
};
var validationChanged = false;
var subscribeToValidationChanged = function() {
if (validatorsController.validationInfoIsDefined()) {
validatorsController.validationInfo.validator.watchValidationChanged(function() {
validationChanged = true;
showErrorInfoIfNeeded();
});
// setup a watch on rule errors if it's not already set
watchCustomErrors(validatorsController.validationInfo);
}
};
var getErrorMessage = function(value) {
var validationInfo = getValidationInfo();
if (!validationInfo) {
return '';
}
var errorMessage = "";
var errors = validationInfo.validator.errors[validationInfo.ruleSetName];
var rules = validationInfo.validator.validationRules[validationInfo.ruleSetName];
for (var errorCode in errors) {
if (errors[errorCode]) {
var errorCodeRule = _.findWhere(rules, {errorCode: errorCode});
if (errorCodeRule) {
errorMessage += errorCodeRule.validate(value).errorMessage;
break;
}
}
}
return errorMessage;
};
var showErrorInfoIfNeeded = function() {
var validationInfo = getValidationInfo();
if (!validationInfo) {
return;
}
var needsAttention = validatorsController.ruleSetHasErrors() && (errorElementController && errorElementController.$dirty || validationChanged);
if (needsAttention) {
// compose and show error message
var errorMessage = getErrorMessage(element.val());
// set and show error message
element.text(errorMessage);
element.show();
} else {
element.hide();
}
};
subscribeToValidationChanged();
if (errorElementController)
{
scope.$watch(function() {
return errorElementController.$dirty;
}, showErrorInfoIfNeeded);
}
scope.$watch(function() {
return validatorsController.validationInfoIsDefined();
}, subscribeToValidationChanged());
});
}
};
}]);
})(angular, $);