Scenario
I am developing a savedialog which accepts any simple object with string, boolean and date fields. I've constructed a component which handels the construction of this, now I would like to dynamically add validation to the input fields which have been created.
I'm programming in TypeScript, using Angular2 and Also a framework called PrimeNG which is the Angular2 version of PrimeFaces.
My code at the moment
Html
<p-dialog #dialog header="{{dialogTitle}}" [(visible)]="isDisplayed" [responsive]="true" showEffect="fade"
[modal]="true" [closable]="false" [closeOnEscape]="false" [modal]="true" [width]="1000" [resizable]="false">
<form novalidate #form (ngSubmit)="save()">
<div class="ui-grid ui-grid-responsive ui-fluid" *ngIf="saveObject">
<div *ngFor="let i of rows" class="ui-grid-row">
<div class="ui-grid-col-6" *ngFor="let field of fields | slice:(i*itemsPerRow):(i+1)*itemsPerRow">
<div class="ui-grid-col-5 field-label">
<label for="attribute">{{field.key}}</label>
</div>
<div class="ui-grid-col-5">
<p-checkbox *ngIf="booleanFields.indexOf(field.key) !== -1" binary="true"
[(ngModel)]="saveObject[field.key]" name="{{field.key}}"
[ngClass]="{'error-border-class': field.key.errors && (field.key.dirty || field.key.touched)}"
[valueValidator]="field.key" [validateObject]="saveObject"></p-checkbox>
<p-calendar *ngIf="dateFields.indexOf(field.key) !== -1" [(ngModel)]="saveObject[field.key]"
name="{{field.key}}"
[ngClass]="{'error-border-class': field.key.errors && (field.key.dirty || field.key.touched)}"
dateFormat="dd/mm/yy" [showIcon]="true" [appendTo]="dialog"
[monthNavigator]="true"
[ngClass]="{'error-border-class': field.key.errors && (field.key.dirty || field.key.touched)}"
[valueValidator]="field.key"
[validateObject]="saveObject"></p-calendar>
<input *ngIf="(booleanFields.indexOf(field.key) === -1) && (dateFields.indexOf(field.key) === -1)"
pInputText id="attribute" [(ngModel)]="saveObject[field.key]" name="{{field.key}}"
[ngClass]="{'error-border-class': field.key.errors && (field.key.dirty || field.key.touched)}"
[valueValidator]="field.key" [validateObject]="saveObject"/>
</div>
</div>
</div>
</div>
</form>
<p-footer>
<div class="ui-dialog-buttonpane ui-widget-content ui-helper-clearfix">
<button label=" " type="button" pButton (click)="hideDialog(true)">
<i class="fa fa-times fa-fw" aria-hidden="true"></i>Sluit
</button>
<button label=" " type="submit" pButton (click)="hideDialog(false)" [disabled]="!form.valid">
<i class="fa fa-check fa-fw" aria-hidden="true"></i>Opslaan
</button>
</div>
</p-footer>
SaveDialogComponent
import { Component, Input, Output, OnChanges, EventEmitter } from '#angular/core';
import { FieldUtils } from '../utils/fieldUtils';
import { ValueListService } from '../value-list/value-list.service';
#Component({
moduleId: module.id,
selector: 'save-dialog',
templateUrl: 'savedialog.component.html',
styleUrls: ['savedialog.component.css']
})
export class SaveDialogComponent implements OnChanges {
#Input() dialogTitle:string;
#Input() isDisplayed:boolean;
#Input() saveObject:any;
#Input() unwantedFields:string[] = [];
#Input() booleanFields:string[] = [];
#Input() dateFields:string[] = [];
#Output() hide:EventEmitter<boolean> = new EventEmitter<boolean>();
#Output() result:EventEmitter<Object> = new EventEmitter<any>();
private fields:any[];
private itemsPerRow:number = 2;
private rows:any[];
ngOnChanges() {
this.fields = FieldUtils.getFieldsWithValues(this.saveObject, this.unwantedFields);
this.rows = Array.from(Array(Math.ceil(this.fields.length / this.itemsPerRow)).keys());
}
hideDialog(clearChanges:boolean) {
if(clearChanges){
this.resetObject()
}
this.isDisplayed = false;
this.hide.emit(this.isDisplayed);
}
save() {
this.result.emit(this.saveObject);
}
private resetObject() {
for(let field of this.fields) {
this.saveObject[field.key] = field.value;
}
}
private getDate(date:string):Date {
return new Date(date);
}
}
I'm Not including field utils since this it isn't that important, basically It extracts all the fields from an object, uses the booleanFields and dateFields to order the fields inorder to place all the fields of a same type next to one another.
ValidatorDirective
import { Directive, Input } from '#angular/core';
import { AbstractControl, ValidatorFn, Validator, NG_VALIDATORS, FormControl } from '#angular/forms';
#Directive({
selector: '[valueValidator][ngModel]',
providers: [
{provide: NG_VALIDATORS, useExisting: ValueValidatorDirective, multi: true}
]
})
export class ValueValidatorDirective implements Validator {
#Input('valueValidator') fieldName:string;
#Input() validateObject:any;
#Input() values:any[];
datumStartValidator:ValidatorFn;
datumEindValidator:ValidatorFn;
codeValidator:ValidatorFn;
naamValidator:ValidatorFn;
volgNrValidator:ValidatorFn;
validate(c:FormControl) {
this.instantiateFields();
switch (this.fieldName) {
case 'datumStart':
return this.datumStartValidator(c);
case 'datumEind':
return this.datumEindValidator(c);
case 'code':
return this.codeValidator(c);
case 'naam':
return this.naamValidator(c);
case 'volgnr':
return this.volgNrValidator(c);
default :
return {
error: {
valid: false
}
}
}
}
instantiateFields() {
this.datumStartValidator = validateStartDatum(this.validateObject['datumEind']);
this.datumEindValidator = validateEindDatum(this.validateObject['datumStart']);
this.codeValidator = validateCode();
this.naamValidator = validateNaam();
this.volgNrValidator = validateVolgNr(null);
}
}
//We'll need multiple validator-functions here. one for each field.
function validateStartDatum(datumEind:Date):ValidatorFn {
return (c:AbstractControl) => {
let startDatum:Date = c.value;
let isNotNull:boolean = startDatum !== null;
let isBeforeEind:boolean = false;
if (isNotNull) {
isBeforeEind = datumEind.getTime() > startDatum.getTime();
}
if (isNotNull && isBeforeEind) {
return null
} else {
return returnError();
}
}
}
function validateEindDatum(startDatum:Date):ValidatorFn {
return (c:AbstractControl) => {
let eindDatum:Date = c.value;
let isNotNull:boolean = eindDatum !== null;
let isBeforeEind:boolean = false;
if (isNotNull) {
isBeforeEind = eindDatum.getTime() > startDatum.getTime();
}
if (isNotNull && isBeforeEind) {
return null
} else {
return returnError();
}
}
}
function validateCode():ValidatorFn {
return (c:AbstractControl) => {
let code:string = c.value;
if (code !== null) {
return null;
} else {
return returnError();
}
}
}
function validateNaam():ValidatorFn {
return (c:AbstractControl) => {
let naam:string = c.value;
if (naam !== null) {
return null;
} else {
return returnError();
}
}
}
function validateVolgNr(volgnummers:string[]):ValidatorFn {
return (c:AbstractControl) => {
return returnError();
}
}
function returnError():any {
return {
error: {
valid: false
}
}
}
My problem
First of all I want to say thank you for reading this all the way, you're a champ! I know my code is not the most well writen beautiful code you've ever seen but I'm just trying to get it to work at the moment. The problem I have at the moment is that Validation happens but I can't seem to set the class using
[ngClass]="{'error-border-class': field.key.errors && (field.key.dirty || field.key.touched)}"
field.key is found no problem But I don't seem to be able to use that as reference to the dynamic name given to the input fields. field.key.errors is undefined which I guess is to be expected. But I haven't found how to fix this yet.
Secondly I think that the way I have It now I'm going to end up having trouble with the validation of the entire form (to disable the submit button)
<button label=" " type="submit" pButton (click)="hideDialog(false)" [disabled]="!form.valid">
Any Ideas on how to fix this are welcome, I'm open to all suggestions.
I'm looking for some way to pass data from a Polymer form fields to REST API,
actually, I'm using core-ajax to do it but I think is a bit heavy method to do it.
Are any standard way to do it?
This is my code:
<template>
<section>
<file-input class="blue" id="file" extensions='[ "xls" ]' maxFiles="1">{{ FileInputLabel }}</file-input>
</section>
<section>
<paper-button raised class="blue" disabled?="{{ (! Validated) || (Submitted) }}" on-tap="{{ Submit }}">
<core-icon icon="send"></core-icon>
Process
</paper-button>
</section>
<paper-toast id="toast" text=""></paper-toast>
<core-ajax id="ajax" url="/import-pdi" method="POST" handleAs="json" response="{{ response }}" on-core-complete="{{ SubmitFinished }}"></core-ajax>
</template>
<script>
Polymer("import-pdi-form", {
Validated: false,
Submitted: false,
FileInputLabel: "SELECT",
ready: function () {
this.shadowRoot.querySelector("#file").addEventListener("change", function(event) {
var container = document.querySelector("import-pdi-form");
container.Validated = (event.detail.valid.length != 0);
if (event.detail.valid.length == 0) {
container.shadowRoot.querySelector("#toast").text = "Invalid Format";
container.shadowRoot.querySelector("#toast").show();
container.FileInputLabel = "SELECCIONA L'ARXIU";
}
else {
container.FileInputLabel = event.detail.valid[0].name;
var form_data = new FormData();
form_data.append("file", event.detail.valid[0], event.detail.valid[0].name);
container.shadowRoot.querySelector("#ajax").body = form_data;
container.shadowRoot.querySelector("#ajax").contentType = null;
}
});
},
Submit: function() {
if ((this.Validated) && (! this.Submitted)) {
this.Submitted = true;
this.shadowRoot.querySelector("#ajax").go();
}
},
SubmitFinished: function(event, detail, sender) {
if (detail.xhr.status == 200) {
this.shadowRoot.querySelector("#toast").text = JSON.parse(detail.xhr.response).message;
}
else {
this.shadowRoot.querySelector("#toast").text = "Server Error";
}
this.shadowRoot.querySelector("#toast").show();
this.FileInputLabel = "SELECCIONA L'ARXIU";
this.shadowRoot.querySelector("#file").reset();
this.Submitted = false;
}
});
</script>
For submitting a form that contains custom elements we currently recommend that you use the ajax-form element. It looks like you may already be using the file-input element by the same author, so the two should work well together.
Today our user reported that saving his CV does not work, that it does not save his Skills, languages, driving license level, schools and prev. employments.
That is the Collection forms that i use on 2 parts of website (CV and Offers)...
Funny is that we tested it before going live and running live from IE6 to any other newer browser.
Collections is added correctly using "add foobar record" button, when there is any record in DB it appears in edit correctly, when i edit these existing it will be saved, if i remove them than they will be removed.
But when i add new, these new records is not in Form post data. I cant understand if its part of form, html is rendered correctly, why it does not include it in post...
These collections works with Offer entity, saved updated added... no problem. i have checked the controller code, the javascript code, the entity code, the html code, the collection templates, the form types..
Here is DB structure:
here is how i add collection in botz CV and Offer
<div class="tbl">
<div class="row">
<div class="col" style="text-align: center; width: 100%;">Počítačové znalosti</div>
</div>
<div class="divider"></div>
<div class="skills" data="0" data-prototype="{% filter escape %}{% include 'TvarplastTopzamBundle:Collections:SkillCollection.html.twig' with {'form': form.skills.vars.prototype} %}{% endfilter %}">
{% for skill in form.skills %}
<div class="row">
{% include 'TvarplastTopzamBundle:Collections:SkillCollection.html.twig' with {'form': skill} %}
</div>
<script type="text/javascript">$(".skills").data("index", {{ loop.index }});</script>
{% endfor %}
</div>
<div class="row">
<div class="col">
Pridať počítačovú znalosť
</div>
</div>
</div>
Problem cannot be with entity, because if some relation exists in DB that is displayed as collection, and if its edited, it can be changed or removed, and its displayed in post parameters, then entity, form type, cannot be wrong.
but i handle form like this:
public function zivotopisAction(\Symfony\Component\HttpFoundation\Request $request, $showmsg = false) {
if (!$this->get("data")->hasPerm(Role::WORKER, $this->getUser())) {
$message["show"] = true;
$message["text"] = "Nemáte požadované oprávnenia. Stránka nemôže byť zobrazená.";
$message["type"] = "box-red";
return new \Symfony\Component\HttpFoundation\Response($this->renderView("TvarplastTopzamBundle::error.html.twig", array("message" => $message)));
}
$return = array();
$message = array("show" => $showmsg, "type" => "", "text" => "");
if ($message["show"]) {
$message["text"] = "Je nutné vyplniť nasledujúce informácie pre pokračovanie.";
$message["type"] = "box-red";
}
$em = $this->getDoctrine()->getManager();
if (!is_null($this->getUser()->getZivotopis())) {
$zivotopis = $em->getRepository("TvarplastTopzamBundle:Zivotopis")->find($this->getUser()->getZivotopis()->getId());
} else {
$zivotopis = new \Tvarplast\TopzamBundle\Entity\Zivotopis();
}
$originalSkills = new \Doctrine\Common\Collections\ArrayCollection();
if ($zivotopis->getSkills()) {
foreach ($zivotopis->getSkills() as $skill) {
$originalSkills->add($skill);
}
}
$originalLanguages = new \Doctrine\Common\Collections\ArrayCollection();
if ($zivotopis->getLanguages()) {
foreach ($zivotopis->getLanguages() as $language) {
$originalLanguages->add($language);
}
}
$originalDrivingskills = new \Doctrine\Common\Collections\ArrayCollection();
if ($zivotopis->getSkilldriving()) {
foreach ($zivotopis->getSkilldriving() as $skilldriving) {
$originalDrivingskills->add($skilldriving);
}
}
$originalEmployments = new \Doctrine\Common\Collections\ArrayCollection();
if ($zivotopis->getEmployments()) {
foreach ($zivotopis->getEmployments() as $employment) {
$originalEmployments->add($employment);
}
}
$originalSchools = new \Doctrine\Common\Collections\ArrayCollection();
if ($zivotopis->getSchools()) {
foreach ($zivotopis->getSchools() as $school) {
$originalSchools->add($school);
}
}
$form = $this->createForm(new \Tvarplast\TopzamBundle\Form\ZivotopisType(), $zivotopis, array(
'action' => $this->generateUrl('zivotopis'),
));
$form->handleRequest($request);
if ($form->isValid()) {
//var_dump($_POST); die();
foreach ($originalSkills as $skill) {
if (false === $zivotopis->getSkills()->contains($skill)) {
$skill->getZivotopis()->removeElement($zivotopis);
$em->persist($skill);
$em->remove($skill);
}
}
foreach ($originalLanguages as $language) {
if (false === $zivotopis->getLanguages()->contains($language)) {
$language->getZivotopis()->removeElement($zivotopis);
$em->persist($language);
$em->remove($language);
}
}
foreach ($originalDrivingskills as $drivingskill) {
if (false === $zivotopis->getSchools()->contains($drivingskill)) {
$drivingskill->getZivotopis()->removeElement($zivotopis);
$em->persist($drivingskill);
$em->remove($drivingskill);
}
}
foreach ($originalEmployments as $employment) {
if (false === $zivotopis->getEmployments()->contains($employment)) {
$employment->getZivotopis()->removeElement($zivotopis);
$em->persist($employment);
$em->remove($employment);
}
}
foreach ($originalSchools as $school) {
if (false === $zivotopis->getSchools()->contains($school)) {
$school->getZivotopis()->removeElement($zivotopis);
$em->persist($school);
$em->remove($school);
}
}
$zivotopis->upload();
$zivotopis->setBasicUser($this->getUser());
$zivotopis = $form->getData();
$em->persist($zivotopis);
$em->flush();
$message["text"] = ($this->container->get('security.context')->isGranted('ROLE_WORKER') ? "Životopis" : "Profil") . " bol úspešne uložený.";
$message["type"] = "box-yellow";
$message["show"] = true;
}
$return["form"] = $form->createView();
$return["message"] = $message;
return $return;
and my javascript looks like this:
$(document).ready(function() {
$.extend({getDeleteLinkCode: function(div) {
return '<div class="col" style="margin-top: 8px; margin-left: 3px;"><a href="#" style="margin-top: 5px;" >Odstrániť</a></div>';
}});
$.extend({addSubFormSelectChangeListener: function(collectionHolder, formRow, div) {
formRow.find('select' + (!div ? ':first' : '')).on("change", function() {
var org = $(this);
if (collectionHolder.find(!div ? "tr" : "div").size() > 1) {
collectionHolder.find(!div ? "tr" : "div").each(function() {
if (org.val() === $(this).find('select:first').val() && org.attr("id") !== $(this).find("select:first").attr("id")) {
org.parent().parent().remove();
}
});
}
});
}});
$.extend({addSubForm: function(collectionHolder, div) {
var prototype = collectionHolder.data('prototype');
var index = collectionHolder.data('index');
index = (index !== parseInt(index) ? 0 : index);
var form = prototype.replace(/__name__/g, index);
var formRow = $((div ? '<div class="row"></div>' : '<tr></tr>')).append(form);
var removeFormRow = $($.getDeleteLinkCode(div));
formRow.append(removeFormRow);
collectionHolder.data('index', index + 1);
collectionHolder.append(formRow);
removeFormRow.on('click', function(e) {
e.preventDefault();
formRow.remove();
});
$.addSubFormSelectChangeListener(collectionHolder, formRow, div);
}});
function addSubFormItemDeleteLink(collectionHolder, $tagFormLi, div, notag) {
var $removeFormA = $($.getDeleteLinkCode(div));
$tagFormLi.append($removeFormA);
$removeFormA.on('click', function(e) {
e.preventDefault();
$tagFormLi.remove();
});
$.addSubFormSelectChangeListener(collectionHolder, $tagFormLi, div);
}
jQuery.fn.toggleOption = function(show) {
$(this).toggle(show);
if (show) {
if ($(this).parent('span.toggleOption').length) {
$(this).unwrap();
}
} else {
if ($(this).parent('span.toggleOption').length === 0) {
$(this).wrap('<span class="toggleOption" style="display: none;" />');
}
}
};
$.extend({comboFilter: function(inputField, comboBox) {
$("#" + inputField).delayBind("input", function() {
var inputValue = $(this).val().toLowerCase();
var combobox = document.getElementById(comboBox);
$("#" + comboBox).children("span").children("optgroup").each(function() {
$(this).toggleOption(true);
});
optionToSelect = false;
$("#" + comboBox + " option").each(function() {
if ($(this).text().toLowerCase().replace(/<.+?>/g, "").replace(/\s+/g, " ").indexOf(inputValue.replace(/<.+?>/g, "").replace(/\s+/g, " ")) !== -1) {
optionToSelect = $(this);
$(this).toggleOption(true);
} else {
$(this).toggleOption(false);
}
if (optionToSelect !== false) {
$(optionToSelect).select();
}
});
$("#" + comboBox).children("optgroup").each(function() {
if ($(this).children("option").length <= 0) {
$(this).toggleOption(false);
} else {
$(this).toggleOption(true);
}
});
if ($("#" + comboBox).children("optgroup").length <= 0) {
$("#" + comboBox).children("span").children("optgroup").children("option").each(function() {
$(this).parent().toggleOption(true);
});
}
if (inputValue === '') {
combobox[0].selected = true;
$("#" + comboBox).children("span").children("optgroup").each(function() {
$(this).toggleOption(true);
});
}
}, 50);
}});
/* skills */
holderSkills = $('div.skills');
holderSkills.find('div.row').each(function() {
addSubFormItemDeleteLink(holderSkills, $(this), false, false);
});
$(".add_skill_link").on('click', function(e) {
e.preventDefault();
$.addSubForm(holderSkills, true);
});
/* driving */
holderDriving = $('div.skilldriving');
holderDriving.find('div.deletehere').each(function() {
addSubFormItemDeleteLink(holderDriving, $(this), true, true);
});
$(".add_driving_link").on('click', function(e) {
e.preventDefault();
$.addSubForm(holderDriving, true);
});
/* Lang */
holderLanguages = $('div.languages');
holderLanguages.find('div.row').each(function() {
addSubFormItemDeleteLink(holderLanguages, $(this), false, false);
});
$(".add_lang_link").on('click', function(e) {
e.preventDefault();
$.addSubForm(holderLanguages, true);
});
/* Emp */
holderEmployments = $('div.employments');
holderEmployments.find('div.deletehere').each(function() {
addSubFormItemDeleteLink(holderEmployments, $(this), false, false);
});
$(".add_zam_link").on('click', function(e) {
e.preventDefault();
$.addSubForm(holderEmployments);
});
/* Schools */
holderSchools = $('div.schools');
holderSchools.find('div.deletehere').each(function() {
addSubFormItemDeleteLink(holderSchools, $(this), false, false);
});
$(".add_Schools_link").on('click', function(e) {
e.preventDefault();
$.addSubForm(holderSchools);
});
});
Any idea where can the problem be?
Thank you very much
Thanks to Dynamically added form field not showing up in POSTed data
Solved by switching first 2 lines to be {form} first and div the second
i had something like
<div .....>
{form....}
some html
</div>
<div>
htmlhhtml
</div>
</div>
{endform}
That was the reason why browser was not able to read newly added items
I want to get all labels inside a div, the blow piece of code works in Firefox and not working IE. Any idea. Thanks in advance.
<div id='discounts'>
<label id="discount1"> discount 1</label>
<label id="discount2"> discount 2 </label>
<input type="text" id="discountmisc" value="" />
</div>
var selectLabels = {
getLabels: function() {
$('#discounts > label').each(function(index, item) {
alert(index + $(item).attr('id'));
});
}
};
selectLabels.getLabels();
Are you wrapped in DOM Ready functions? i.e.
$(function () {
var selectLabels = {
getLabels: function() {
$('#discounts > label').each(function(index, item) {
alert(index + $(item).attr('id'));
});
}
};
selectLabels.getLabels();
});
or alternately:
var selectLabels = {
getLabels: function() {
$('#discounts > label').each(function(index, item) {
alert(index + $(item).attr('id'));
});
}
};
$(selectLabels.getLabels);
or finally (because you don't care about the return value):
var selectLabels = {
getLabels: function() {
$(function () {
$('#discounts > label').each(function(index, item) {
alert(index + $(item).attr('id'));
});
});
}
};
selectLabels.getLabels();
Tell me, and if so, I'll change my answer.
I have the following code in my view:
<% using (Ajax.BeginForm("JsonCreate", new AjaxOptions { OnComplete = "createCategoryComplete" }))
{ %><div id="createCategory">
<fieldset>
<legend>Add new category</legend>
<p>
<%=Html.TextBox("CategoryId")%>
<%=Html.TextBox("Test")%>
<label for="name">Name:</label>
<%= Html.TextBox("Name")%>
<%= Html.ValidationMessage("Name")%>
</p>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
</div>
In the controller the code is as follows:
[AcceptVerbs(HttpVerbs.Post)]
public JsonResult JsonCreate(string Name)
{
if (ModelState.IsValid)
{
try
{
//Return a json object to the javascript
return Json(new { CategoryId = 123, Test= "test successful" });
}
catch
{
#region Log errors about the exception
//Log error to administrator here
#endregion
}
}
//If we got this far, something failed, return an empty json object
return Json(new { /* Empty object */ });
}
What should be the code in the view for the following function to read the values returned by the Json and update the textboxes for CategoryId and Test?
function createCategoryComplete() {....???}
function createCategoryComplete(e) {
var obj = e.get_object();
alert(obj.CategoryId + ' ' + obj.Test);
}
Try this,
success: function(data) {
alert(data.CategoryId + " " + data.Test);
EDIT:
function createCategoryComplete(data)
{
document.getElementById("UrTxtBoxID").value = data.Test;
}