Angular Formly Error Summary when using fieldGroups - javascript

Hopefully this will be a more generic Angular JS question rather than something specific to Angular Formly.
I've been following the framework provided here for building an error summary on the angular formly form. All works well.....but!
In the example, their model is as follows:
vm.fields = [
{
key: 'picky',
type: 'customInput',
templateOptions: {
label: 'Picky field...',
placeholder: 'This is required and has a maxlength of 5 and minlength of 3',
required: true,
maxlength: 5,
minlength: 3
}
},
.....
{
key: 'ip',
type: 'customInput',
validators: {
ipAddress: {
expression: function(viewValue, modelValue) {
var value = modelValue || viewValue;
return /(\d{1,3}\.){3}\d{1,3}/.test(value);
},
message: '$viewValue + " is not a valid IP Address"'
}
},
templateOptions: {
label: 'IP Address',
required: true,
type: 'text',
placeholder: '127.0.0.1',
}
}
];
Then, if we look at the HTML, we can see that these fields are being passed into the error summary as such:
<formly-error-summary form="vm.form" fields="vm.fields"></formly-error-summary>
For simple form structure, this works fine, but, if you want to use a Bootstrap layout, as described here then your model ends up looking something mine does:
vm.rentalFields = [
{
template: '<div class="row"><div class="col-xs-12"><h3>About You</h3></div></div>'
},
{
className: 'row',
fieldGroup: [
{
className: 'col-xs-6',
type: 'customInput',
key: 'first_name',
templateOptions: {
type: 'text',
label: 'First Name',
placeholder: 'Enter your first name',
required: true
}
},
{
className: 'col-xs-6',
type: 'customInput',
key: 'last_name',
templateOptions: {
type: 'text',
label: 'Last Name',
placeholder: 'Enter your last name',
required: true
},
expressionProperties: {
'templateOptions.disabled': '!model.first_name'
}
}
]
},
{
template: '<div class="row"><div class="col-xs-12"><h3>License and Insurance Details</h3></div></div>',
hideExpression: '!model.email'
}
.....
Now, when we pass in vm.rentalFields to the error summary, instead of accessing the fields, it instead just validates each object. I can get round this by doing something like:
<formly-error-summary form="vm.rentalForm" fields="vm.rentalFields[1].fieldGroup"></formly-error-summary>
This of course is not ideal since there will be fields in other field groups that I will want to validate, for proving the issue though it's fine for now. I have tried just passing in 'vm.rentalFields.fieldGroup' but as I suspected, that returns nothing.
So, is there a way I can recursively pass in all the fieldGroups within the vm.rentalField object or is this something that I should handle within the code of the Directive itself.
angular.module("formlyApp").directive('formlyErrorSummary', function() {
return {
scope: {},
bindToController: {
form: '=',
fields: '='
},
templateUrl: 'js/Directives/formly-error-summary.html',
controllerAs: 'vm',
controller: function() {
var vm = this;
vm.getErrorAsList = getErrorAsList;
console.log(vm.fields);
function getErrorAsList(field) {
return Object.keys(field.formControl.$error).map(function(error) {
// note, this only works because the customInput type we have defined.
return field.data.getValidationMessage(error);
}).join(', ');
}
}
};
});
EDIT
Ok, so, after taking advice from Ken below, I have been able to modify my formlyErrorSummary directive so that it now is at least able to get the errors for the model. This has numerous issues in it since the $scope.$watch is doing a deep level comparison and even on the first page load, the whole thing is fired 3 times! I've added in some rudimentary escapes to try and combat this and for now at least I have the errors, the next issue I have is within the HTML where I am call ng-repeat="field in vm.fields" which is effectively the same issue, so how would I work round this? Part of me is thinking of some anonymous object that would hold the fields message and whether or not is is valid and then parse that inside the HTML, but I'm not sure if this way of thinking is applicable to Angular?
controller: function($scope) {
var vm = this;
$scope.$watch('vm.fields', function(){
for(var i = 0; i < vm.fields.length; i++)
if(vm.fields[i].fieldGroup) {
for(var j = 0; j < vm.fields[i].fieldGroup.length; j ++)
if(vm.fields[i].fieldGroup[j].formControl) {
var err = getErrorAsList(vm.fields[i].fieldGroup[j]);
if(err)
vm.getErrorAsList = err;
}
}
}, true);
SOLUTION - POSSIBLY
After much hacking around, I think I finally have this working so that the error messages are now displayed both inline and in a summary at the top.
My final directive function now creates an array each time it is run which will hold all of the error messages, it has to be flushed within the $watch otherwise when the field is valid, the error message will persist within the array so we simply rebuild the entire thing each time.....given I'm already using a deep level watch here, I'm hoping any performance hits will be negligible.
vm.errs = [];
$scope.$watch('vm.fields', function(){
vm.errs = [];
for(var i = 0; i < vm.fields.length; i++)
if(vm.fields[i].fieldGroup) {
for(var j = 0; j < vm.fields[i].fieldGroup.length; j ++)
if(vm.fields[i].fieldGroup[j].formControl) {
var err = getErrorAsList(vm.fields[i].fieldGroup[j]);
if(err)
if(vm.errs.indexOf(err) === -1)
vm.errs.push(err);
}
}
}, true);
Then, within the directives template, I had to remove the vm.fields reference as that was obviously not going to work in this approach. Since I knew that this summary would only be shown if the form was invlaid, I could remove so of the other checks that were being carried out and eventually ended up with the following HTML:
<div class="row">
<div class="col-xs-12">
<div class="formly-error-summary bg-danger" ng-if="vm.form.$invalid">
<div ng-repeat="err in vm.errs" class="color-error">
<i class="glyphicon glyphicon-remove"></i>
<span>
{{err}}
</span>
</div>
</div>
</div>
</div>
I'm still not 100% happy with this, it gets the job done, but I'm not sure if it's the 'Angular' way of doing this and the fact I'm using $scope.$watch on the fields object is a little bit annoying to my developer OCD, but a solution it is all the same.
If anyone has any refinements or suggestions for improvements to this let me know please, still getting to grips with Angular but this has been a pretty fun learning experience!

Related

Kendo scheduler custom view doesn't get the right class when selected

I am currently working on a kendo scheduler.
I was asked by my client to implement a 3-days view, which I successfully did, but there is one problem : that custom view does not get the "k-state-selected" class when it is selected, which means it can't be fully stylized.
I failed to find why that could be the case : none of the examples for creating a custom time view I found mentionned anything about defining the class the view takes when selected, and furthermore, it does get the "k-state-hover" class when hovered. Strange.
Here's the (I think) relevant JS :
var ThreeDayView = kendo.ui.MultiDayView.extend({
nextDate: function () {
return kendo.date.nextDay(this.startDate());
},
options: {
selectedDateFormat: "{0:D} - {1:D}"
},
name: "ThreeDayView",
calculateDateRange: function () {
//create a range of dates to be shown within the view
var start = this.options.date,
idx, length,
dates = [];
for (idx = 0, length = 3; idx < length; idx++) {
dates.push(start);
start = kendo.date.nextDay(start);
}
this._render(dates);
}
});
$("#scheduler").kendoScheduler({
date: new Date(), // The current date of the scheduler
showWorkHours: true,
height: 600,
views: [
"week",
{ type: ThreeDayView, title: "3 Jours", selected: false },
"day"
],
editable:
{
resize: true,
move: true,
template: $("#templateEdition").html()
},
dataSource: finalSource,
add: onAdd,
edit: onUpdate,
remove: onDelete,
save: onSaving
})
});
Does anyone have any idea why that might be ?
Thanks !
The type or the view should be a string - the name of the custom view - "ThreeDayView" (instead of ThreeDayView) in this case.

Bind OracleJet ojtimeline component to viewModel

I am trying to understand how I can bind data from the view-model to the view. The REST request to the back-end is working fine and I get a JSON array with several items. The existing documentation doesn't give me enough help.
How can I bind the timeline component ojtimeline to the view-model data array?
Edit: No errors now, since the view recognize the view-model array. But the ojtimeline doesn't display the data, only a working empty view component.
View
<div id="tline"
data-bind='ojComponent: {
component: "ojTimeline",
minorAxis: {
scale: "hours",
zoomOrder: ["hours", "days", "weeks"]
},
majorAxis: {
scale: "weeks"
},
start: new Date("Jan 1, 2016").toISOString(),
end: new Date("Jun 31, 2016").toISOString(),
referenceObjects: [{value: new Date("Feb 1, 2010").toISOString()}],
series: [{
id: "id",
emptyText: "No Data.",
items: statusArray,
label: "Oracle Events"
}],
overview: {
rendered: "off"
}
}' style="width: '100%';height: 350px"></div>
View-model
define(['ojs/ojcore', 'knockout', 'jquery', 'ojs/ojknockout', 'ojs/ojtimeline'],
function (oj, ko) {
/**
* The view model for the main content view template
*/
function timelineContentViewModel() {
var self = this;
this.statusArray = ko.observableArray([]);
self.addData = function () {
$.ajax({
url: "http://localhost:8080/myproject/rest/status/v1/findAll",
type: 'GET',
dataType: 'json',
success: function (data, textStatus, jqXHR) {
var x = data;
for (i = 0; i < x.length; i++) {
statusArray.push({
id: data[i].id,
description: data[i].text,
title: data[i].user.screenName,
start: data[i].createdAt});
}
//$("#tline").ojTimeline("refresh"); Doesn't have ant affect
}
});
};
self.addData();
}
return timelineContentViewModel;
});
The ReferenceError is caused by
var statusArray = ko.observableArray([]);
it should be
this.statusArray = ko.observableArray([])
You will also (probably) need to refresh the timeline when the observable array has changed, e.g. after the for-loop in success callback:
...
success: function (data, textStatus, jqXHR) {
var x = data;
for (i = 0; i < x.length; i++) {
self.statusArray.push({
id: data[i].id,
description: data[i].text,
title: data[i].user.screenName,
start: data[i].createdAt});
}
$("#tline").ojTimeline("refresh");
}
...
I have loaded ojTimeline from Ajax data and have never needed to use refresh. Worst case, you can wrap the ojTimeline in a <!-- ko if ... --> so that the timeline doesn't appear until you have an Ajax response.
For the ojTimeline items attribute, instead of referencing the observable, I had to unwrap the observable like this: items: ko.toJS(statusArray).
Another thing to consider is pushing into an ko.observableArray inside a for loop. Each push using the ko.observableArray push() method invokes subscriptions. If your array is bound to the UI, then each push will trigger a DOM change. Instead, it is often better to push into the underlying array (unwrap the array) and then invoke self.statusArray.valueHasMutated. You may also want to keep an eye on your use of this, self, and nothing. Consistency will help avoid bugs like the one ladar identified.
What do you think about rewriting your for loop like this (code untested)?
ko.utils.arrayPushAll(
self.statusArray(),
ko.utils.arrayMap(data, function(item) {
return {
id: item.id,
description: item.text,
title: item.user.screenName,
start: item.createdAt;
};
});
);
self.statusArray.valueHasMutated();
Or, if you can get away with it (some OJ components don't like this approach), you can skip the push and just replace the entire array inside the observable:
self.statusArray(
ko.utils.arrayMap(data, function(item) {
return {
id: item.id,
description: item.text,
title: item.user.screenName,
start: item.createdAt;
};
});
);

Ionic - Didn't return value $scope in $ionicPopup.show inside factory

I created a project with $ionicPopup. I put the $ionicPopup code in a .factory. In my $ionicPopup.show() I ask user to input a value. After users already input the value, then it will alert the value what the user's wrote.
I also checked the following post, but still cannot solve my problem Access scope inside an angular js factory .
So here is my code:
controller
.controller('PopupCtrl',function($scope, $ionicPopup, $timeout, popupService) {
// Triggered on a button click, or some other target
$scope.showPopup = function() {
var showParameter = {
title: "Test",
cssClass: "",
subTitle: "Insert any value",
template: '<input type="text" ng-model="value">',
templateUrl: "",
buttons: {
cancelText: "Reject",
cancelType: "button-assertive",
okText: "Accept",
okType: "button-positive"
}
}
// An elaborate, custom popup
popupService.show(showParameter, $scope).then(function(res) {
console.log('Tapped!', res);
alert("value: " + res);
});
};
})
factory
.factory('popupService', function ($ionicPopup) {
return{
show: function(param, scope){
var show = $ionicPopup.show({
title: param.title, // String. The title of the popup.
cssClass: param.cssClass, // String, The custom CSS class name
subTitle: param.subTitle, // String (optional). The sub-title of the popup.
template: param.template, // String (optional). The html template to place in the popup body.
templateUrl: param.templateUrl, // String (optional). The URL of an html template to place in the popup body.
scope: scope, // Scope (optional). A scope to link to the popup content.
buttons: [{ // Array[Object] (optional). Buttons to place in the popup footer.
text: param.buttons.cancelText,
type: param.buttons.cancelType,
onTap: function(e) {
return false;
}
}, {
text: param.buttons.okText,
type: param.buttons.okType,
onTap: function(e) {
// Returning a value will cause the promise to resolve with the given value.
return scope.value;
}
}]
});
return show;
}
}
});
DEMO: http://codepen.io/aishahismail/pen/pgpdGW?editors=101
Your help is really need. Thank you.
Due to JS (and Angular) object inheritance[1] you have to "wrap" primitives in objects, so here is the working code (forked from yours):
http://codepen.io/beaver71/pen/JGMvdV
The key edits are these:
$scope.data = {};
...
template: '<input type="text" ng-model="data.value">'
[1] Popup inherits its scope from the controller. See in general: https://github.com/angular/angular.js/wiki/Understanding-Scopes

How to reference form field created by AngularJS formly

I use Formly for creating my forms in angularJS
This is my field
$scope.postFields = [
{
key: 'title',
type: 'input',
templateOptions: {
label: "Title",
// required: true,
minlength: 2,
},
validation: {
messages: {
required: function(viewValue, modelValue, scope) {
return scope.to.label + ' is required'
}
}
}
}
]
and I'm trying to access my fields as follows
function failure(response) {
console.log("failure", response)
_.each(response.data, function(errors, key) {
_.each(errors, function(e) {
$scope.form[key].$dirty = true;
$scope.form[key].$setValidity(e, false);
});
});
}
my formly form
<formly-form form="postForm" model="model" fields="postFields" class="col-md-4">
<button type="submit" class="btn btn-primary" ng-click="addPost()">Submit</button>
</formly-form>
but of course I'm getting this error:
TypeError: Cannot read property 'title' of undefined
it's on this line
$scope.form[key].$dirty = true;
do anyone of you know how to reference created formly fields the right way?
If you want to be able to reference the fields from the form, you could provide a name attribute to your fields. The name is generated by default. It's one of the nice things that angular-formly provides (you don't have to think about it). But if you want to reference it directly with a specific key (as you are) then you'd be best served by providing one yourself.
So you'd do something like this:
$scope.postFields = [
{
key: 'title',
name: 'title',
type: 'input',
templateOptions: {
label: "Title",
// required: true,
minlength: 2,
},
validation: {
messages: {
required: function(viewValue, modelValue, scope) {
return scope.to.label + ' is required'
}
}
}
}
]
Alternatively, you could create a fieldTransform to do this automatically (just assign the name the same as the key). Or in your error handler, you could look up the NgModelController from the field's formControl property like this:
function handleError(fields, response) {
_.each(fields, function(field) {
if (response.data[field.key]) {
field.formControl.$setDirty()
_.each(response.data[field.key], function(e) {
field.formControl.$setValidity(e, false)
})
}
})
}
That's probably the best solution :-) Good luck!

jTable Conditional show\hide edit and delete buttons based on owner of data

Im using jTable to display CDs info and a child table to show reviews of that CD. I want to be able to only show the edit\delete buttons on the rows for the user that is logged in. I have been trying to follow the suggestions made on: https://github.com/hikalkan/jtable/issues/113
https://github.com/hikalkan/jtable/issues/893
https://github.com/hikalkan/jtable/issues/620
Can honestly say im not having much luck with any of these examples. We had been told to include some jquery in our assignment so I chose to go with using it for my table data. Im wishing now id just done something very basic!
Working jTable without condition:
display: function (reviewData) {
//Create an image that will be used to open child table
var $img = $('<img class="child-opener-image" src="/Content/images/Misc/list_metro.png" title="List Reviews" />');
//Open child table when user clicks the image
$img.click(function () {
$('#ReviewTableContainer').jtable('openChildTable',
$img.closest('tr'),
{
title: "Your reviews on this album",
actions: {
listAction: 'childReviewActions.php?action=list&ID=' + reviewData.record.CDID,
deleteAction: 'childReviewActions.php?action=delete&ID=' + reviewData.record.CDID,
updateAction: 'childReviewActions.php?action=update&ID=' + reviewData.record.CDID
},
fields: {
userID: {
key: true,
create: false,
edit: false,
list: false
},
userName: {
title: 'User',
edit: false,
width: '20%'
},
reviewDate: {
title: 'Review date',
width: '20%',
type: 'date',
edit: false,
displayFormat: 'dd-mm-yy'
},
reviewText: {
title: 'Review',
type: 'textarea',
width: '40%'
}
},
Issue 620 attempt:
actions: {
listAction: 'childReviewActions.php?action=list&ID=' + reviewData.record.CDID,
#if (reviewData.record.userID == <?php echo mysql_real_escape_string($_SESSION['ID']);?>)
{
deleteAction: 'childReviewActions.php?action=delete&ID=' + reviewData.record.CDID,
updateAction: 'childReviewActions.php?action=update&ID=' + reviewData.record.CDID
}
},
This way gives me compile error: invalid property id on the IF statement.
If I take out the # in the if statement I get: missing : after property id.
Issue 113 & 893 attempt:
actions: {
listAction: {
url:'http://localhost/childReviewActions.php?action=list&ID=' + reviewData.record.CDID
//updateAction: {
//url:'childReviewActions.php?action=update&ID=' + reviewData.record.CDID,
//enabled: function (data) {
//return data.record.userID = <?php echo mysql_real_escape_string($_SESSION['ID']);?>;
//}
//}
},
On this I couldnt even get it to list the contents of the child table. It keeps coming back with 404 not found error: The requested url /[object object] was not found on this server. Has anyone any ideas how to get these examples working on have a different example of how to get the table to enable\enable the edit, update buttons? This is all new to me so I apologise now
rowInserted: function (event, data) {
//After child row loads. Check if the review belongs to the member logged in. If not remove the edit/delete buttons
if (data.record.userID != $user) {
data.row.find('.jtable-edit-command-button').hide();
data.row.find('.jtable-delete-command-button').hide();
}
else{
//If a review record does belong to the user set variable to true so the add new review link can be hidden after all records have been loaded
$memberReviewExists = true;
//Also needed here for when a new record is inserted
$(".jtable-add-record").hide();
}
},
recordsLoaded: function (event, data) {
if (typeof $memberReviewExists != 'undefined' && $memberReviewExists == true){
$(".jtable-add-record").hide();
$memberReviewExists = null;
}
else {
//No review currently exists for this user so show the Add review link $(".jtable-add-record").show();
}
},
recordDeleted: function (event, data) {
//User has deleted their review. Re-show the add new review link
$(".jtable-add-record").show();
}
The following worked for me. It hides the edit/delete button on rows where the current user is not the authorized user. Note: I added a column for authorizedUser in the mysql table and use that to know if the user is allowed or not.
rowInserted: function(event, data){
var $currentUser='<?php echo $_SESSION['email']?>';
if (data.record.authorizedUser != $currentUser) {
data.row.find('.jtable-edit-command-button').hide();
data.row.find('.jtable-delete-command-button').hide();
}
},
#Toni Your code contains asp.net code too. # is ASP.NET Directive.

Categories

Resources