I am trying to add validation to custom element that gets generated in dynamic form component to support page for view. I injected Aurelia-Validation in main.ts, and DynamicForm.ts and instantiated. Below is my code.
CUSTOM ELEMENT:
TS File
import { customElement, useView, bindable, bindingMode, inject } from 'aurelia-framework';
#customElement('custom-input')
#useView('./custominput.html')
export class CustomInput {
#bindable({ defaultBindingMode: bindingMode.twoWay }) fieldValue: string;
#bindable public customClass: string;
#bindable public placeHolder: string;
#bindable public fieldName: string;
#bindable public formItem: any;
}
HTML View:
<template>
<input class="${customClass}" custom-class.bind="customClass" type="text" field-value.bind="fieldValue"
value.two-way="fieldValue & validateOnChange" placeholder="${placeHolder}" place-holder.bind="placeHolder"
id="${fieldName}" field-name.bind="fieldName" form-item.bind="formItem" />
</template>
DynamicForm
TS File:
import { bindable, bindingMode, inject } from 'aurelia-framework';
import { ValidationRules, ValidationControllerFactory } from 'aurelia-validation';
#inject(ValidationControllerFactory)
export class DynamicForm {
#bindable public formName: string;
#bindable public formTemplate: Object;
#bindable public callback;
inputItem: HTMLInputElement;
controller = null;
constructor(ValidationControllerFactory) {
this.controller = ValidationControllerFactory.createForCurrentScope();
}
public formValidator(element, field) {
//console.log(element);
}
public bind() {
if (this.formTemplate) {
this.formTemplate[this.formName].fields.forEach((item, i) => {
if (item.validation.isValidate === true) {
ValidationRules.ensure(item.name)
.displayName(item.name)
.required()
.on(this.formTemplate);
}
});
this.controller.validate();
}
console.log(this.controller);
}
}
HTML View:
<template>
<require from="../../elements/custominput/custominput"></require>
<form class="form-horizontal">
<div form-name.bind="formName" class="form-group" repeat.for="item of formTemplate[formName].fields">
<label for="${item.name}" class="col-sm-2 control-label">${item.label}</label>
<div class="col-sm-10" if.bind="item.type === 'text' && item.element === 'input'">
<custom-input router.bind="router" custom-class="${item.classes}" field-value.two-way="item.value"
place-holder="${item.placeHolder}" ref="inputItem" item.bind="formValidator(inputItem, item)"
field-name.bind="item.name" form-item.bind="item">
</custom-input>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-default pull-right" click.delegate="callback()">Submit</button>
</div>
</div>
</form>
<ul if.bind="controller.errors.length > 0">
<li repeat.for="error of controller.errors">${error}</li>
</ul>
</template>
Support page:
This page will load DynamicForm
<template>
<require from="./support.scss"></require>
<require from="../DynamicForm/dynamicform"></require>
<div class="panel panel-primary">
<div class="panel-heading">${pageTitle}</div>
<div class="panel-body">
<dynamic-form form-template.two-way="formTemplate" form-name.two-way="formName" callback.call="processForm()"></dynamic-form>
</div>
</div>
</template>
When I view the support page in browser, I do not get validation in UI. Not sure if validation is position in in nested components/elements. The view is generated like this custominput element -> DynamicForm -> support page
Plunker link for more information:
Any help is really appreciated. :)
Two major issues:
1. Rules shouldn't be stored on fields
Rules are stored on the prototype of an object and pertain to the properties of that object.
You are defining the rules on each individual property, so ultimately it's trying to validate property.property rather than object.property, which doesn't do much as you can see.
You're also declaring the rules every time the form template changes. I basically wouldn't put that logic there; put it closer to where those object come from.
If the objects are declared somewhere in your client code, declare the rules in the same module files
If the objects come from the server, declare the rules on those objects on the same place where you fetch them, right after you fetched them
Either way, those rule declarations don't belong in a change handler.
2. Bindings are missing
The ValidationController needs to know which object or properties you want to validate. It only knows in either of these cases:
Your rules are declared via controller.addObject(obj, rules).
Your rules are declared via ValidationRules.[...].on(obj) and the fields in your html template have & validate following them.
There's several pros and cons with either approach, ultimately you should go with one that gives you least resistance. I would probably go for the second approach because things get more entangled if you declare all rules on your controllers directly.
Related
From code template we have:
<div class="block #block.CssName()">
<div class="container">
#Html.DisplayFor(m => block, block.GetType().Name)
</div>
</div>
it make my content block always inside container class. How to make a flexible page where we can put a block inside and outside container
You can check the block type and put a different class on the div besides "container" like this:
<!-- language: lang-razor -->
<div class="block #block.CssName()">
<div class="#(block.GetType().Name == "MySpecialBlock" ? "myspecialclass" : "container") ">
#Html.DisplayFor(m => block, block.GetType().Name)
</div>
</div>
or you can remove the div altogether:
<!-- language: lang-razor -->
<div class="block #block.CssName()">
#Html.DisplayFor(m => block, block.GetType().Name)
</div>
with the intention of editing each DisplayTemplate .cshtml file and add a wrapper div there. i.e. /Views/Cms/DisplayTemplates/HtmlBlock.cshtml :
<div class="myWrapperClass">
#Html.Raw(Model)
</div>
Note: If you did this, you'd probably want to edit each of the various block type templates to add a wrapper of some kind.
Another possibility would be to write a helper class that checks the block type and automatically returns a specific class depending on each block type.
using Piranha.Extend;
namespace MyProject.Classes
{
public static class Helper
{
public static string getWrapperCssClassForBlockType (Block block)
{
string blockName = block.GetType().Name;
string className = "";
switch(blockName)
{
case "HtmlBlock":
className = "row";
break;
case "QuoteBlock":
className = "myQuoteClass";
break;
default:
className = "container";
break;
}
return className;
}
}
}
Then just call the helper method from in your template(s):
#using MyProject.Classes;
#foreach (var block in Model.Blocks)
{
var wrapperClass = Helper.getWrapperCssClassForBlockType(block);
<div class="block #block.CssName()">
<div class="#wrapperClass">
#Html.DisplayFor(m => block, block.GetType().Name)
</div>
</div>
}
So finally i've made it by removing containerfrom template:
<div class="block #block.CssName()">
<!--<div class="container">-->
#Html.DisplayFor(m => block, block.GetType().Name)
<!--</div>-->
</div>
It make all standard block sit outside container. Next i made a group block of container. Any block can be inside this container group block except standard group block such as gallery and columns. Luckily it is open source, so we can make our own columns and gallery group block to be inside a container by modify cshtml template. I dont know how piranha.org do this
But i think my solve fit my purpose
My use case is I have the MainLayout.razor with this code
#inherits LayoutComponentBase
<header><nav ....></header>
<section><h1>Page Title</h1><section>
<main class="container">
<div class="row"><div class="col-12">#Body</div></div>
</main>
Now I want to set the page title from every #Body razor fragment (maybe by inheritance)
#page "/about"
<div>....</div>
#code {
Title = "About Title";
}
I want avoid to put <section> inside the #body fragment.
Also have the same problem with the title-element from head-element. What is best practices to do this (without js interop)?
There are a couple of ways to do that...
Using CascadingValue feature
Define a property in MainLayout to get the title from child components such as
the about component.
Add a CascadingValue component to MainLayout, and pass the MainLayout component
as the value of the Value attribute.
In the child component define a CascadingParameter property which stores the
MainLayout object, and assign a title to its Title property
Here's the full code:
MainLayout
<div class="main">
<div class="top-row px-4 auth">
<h1>#Title</h1>
<LoginDisplay />
About
</div>
<div class="content px-4">
<CascadingValue Value="this">
#Body
</CascadingValue>
</div>
</div>
#code
{
string title;
public string Title
{
get => title;
set
{
if(title != value)
{
title = value;
StateHasChanged();
}
}
}
}
About.razor
#page "/about"
<div>....</div>
#code {
[CascadingParameter]
public MainLayout MainLayout { get; set; }
protected override void OnInitialized()
{
MainLayout.Title = "About Title";
}
}
Create a service class that defines a Title property that can be set by
components into which the service is injected. This service class should also provide a way to pass the title supplied by child components to the MainLayout, which should refresh itself in order to display the provided title...
Hope this helps...
I'm building a custom data grid framework for a LOB-style Aurelia app and need help with how to template the main grid element so it can pick up custom cell templates from child column elements for rendering.
This is what I've done so far:
grid-example.html
<data-grid items-source.bind="rowItems">
<data-column property-name="Name" t="[displayName]fields_Name">
<div class="flex -va-middle">
<div class="user-avatar avatar-square">
<img
if.bind="row.dataItem.avatarUri"
src.bind="row.dataItem.avatarUri" />
</div>
<span class="name">${row.dataItem.name}</span>
</div>
</data-column>
<data-column property-name="StatusText" t="[displayName]fields_Status">
<span class="label ${row.statusClass}">${row.dataItem.statusText}</span>
</data-column>
<data-column property-name="Location" t="[displayName]fields_Location">
<span>${row.dataItem.location}</span>
</data-column>
<data-column property-name="DateCreated" t="[displayName]fields_MemberSince">
<span tool-tip.bind="row.dataItem.dateCreated | dateToString:'long-date-time'">
${row.dataItem.dateCreated | dateToString:'short-date'}
</span>
</data-column>
</data-grid>
data-column.ts
import {
autoinject,
bindable,
noView,
processContent,
ViewCompiler,
ViewFactory } from "aurelia-framework";
import { DataGridCustomElement } from "./data-grid";
#autoinject
#noView
#processContent(false)
export class DataColumnCustomElement {
#bindable
propertyName: string;
#bindable
displayName: string;
cellTemplate: ViewFactory;
constructor(private readonly _element: Element,
private readonly _dataGrid: DataGridCustomElement,
private readonly _viewCompiler: ViewCompiler) {
this.cellTemplate = this._viewCompiler.compile(`<template>${this._element.innerHTML}</template>`);
this._dataGrid.columns.push(this);
}
}
data-grid.html
<template>
<div class="table-wrapper -data-list -sticky-header">
<table class="hover unstriped">
<tbody>
<tr class="labels">
<th repeat.for="column of columns">
<span>${column.displayName}</span>
</th>
</tr>
<tr repeat.for="row of itemsSource">
<td repeat.for="column of columns">
<!-- inject view for column.cellTemplate here? -->
</td>
</tr>
</tbody>
</table>
</div>
</template>
data-grid.ts
import { autoinject, bindable } from "aurelia-framework";
import { DataColumnCustomElement } from "./data-column";
#autoinject
export class DataGridCustomElement {
#bindable
itemsSource: any[] = [];
columns: DataColumnCustomElement[] = [];
constructor(private readonly _element: Element) {
}
}
The data-column elements declare a cell template which is parsed manually into a ViewFactory instance - what I'm stuck on is how to use the cell template for each data-column in the corresponding td repeater in the data-grid template, so it behaves as if I had directly declared the template content there.
Is this possible to do with the default repeat.for syntax? Or do I need a custom template controller to do this, which can additionally accept a ViewFactory instance as a bindable parameter from the scope?
If there is a better way to achieve this requirement then I'm open to that too.
You're essentially trying to compile+render dynamic html. There is nothing special about this specific to repeat.for or tables, but depending on what you're trying to achieve this is usually a bit more involved than simply passing html through the viewCompiler.
You can see an example in a plugin I wrote: https://github.com/aurelia-contrib/aurelia-dynamic-html/
I would probably either use that plugin (or simply copy+paste the code and tweak/optimize it to your needs) and then, keeping the rest of your code as-is, do something like this:
data-column.ts
this.cellTemplate = this._element.innerHTML; // just assign the raw html
data-grid.html
<tr repeat.for="row of itemsSource">
<td repeat.for="column of columns"
as-element="dynamic-html"
html.bind="column.cellTemplate"
context.bind="row[column.propertyName]">
</td>
</tr>
In any case, you'll make this easier for yourself if you just use a custom element like this or in some other form. Making your own repeater will be very difficult ;)
I have a form-group. In the form-group, i have a form-array. I have initialized the array with items created from data in the data model with the method as described in angular's documentation on reactive forms, under the section Initialize the secretLairs FormArray. The problem is i need to perform validation.required for each form control in the array. However, since the form-controls already hold values, i do not know how to perform validation.required. The documentation did not go further on how to validate formcontrols in a formarray that are prepopulated.
Here are my source codes:
.html
<!-- list of Questions -->
<div formArrayName="questions">
<!-- <div *ngFor="let que of Questions; let k=index"> -->
<div *ngFor="let question of Ques ; let i=index" [formGroupName]="i" >
<!-- The repeated questions template -->
<h4>{{question.ques}}</h4>
<div style="margin-left: 1em;">
<!-- <div class="form-group">
<label class="center-block">
<input class="form-control" formControlName="ques" >
</label>
</div> -->
<div class="form-group radio" *ngFor="let choice of
question.choices; let j = index">
<input type="radio" formControlName="choices"
class="custom-control-input" [value]="choice.choiceText">
<label>{{choice.choiceText}}</label>
</div>
<br>
<!-- End of the repeated questions template -->
</div>
</div>
</div>
<button type="submit" class="btn btn-danger"
[disabled]="!CheckListForm.valid">Submit</button>
</form>
.ts
export class CheckListFormComponent implements OnInit, OnChanges {
CheckListForm: FormGroup;
Ques: Questions[];
employmenttype = ['Permanent', 'contractor'];
constructor(private fb: FormBuilder,
private checklistservice: ChecklistService) {
this.CreateForm();
}
ngOnInit() {
this.checklistservice.getQuestions(1).subscribe(res =>{ this.Ques =res;
this.setquestions(this.Ques)
});
this.CheckListForm.get('EmploymentType').valueChanges.subscribe(
(EmploymentType: string) => {
if (EmploymentType === 'Permanent') {
this.CheckListForm.get('HRMS').setValidators([Validators.required]);
this.CheckListForm.get('CompanyName')
.setValidators([Validators.nullValidator]);
} else if (EmploymentType === 'contractor') {
this.CheckListForm.get('CompanyName').
setValidators([Validators.required]);
this.CheckListForm.get('HRMS').
setValidators([Validators.nullValidator]);
}
this.CheckListForm.get('HRMS').updateValueAndValidity();
this.CheckListForm.get('CompanyName').updateValueAndValidity();
}
)
}
CreateForm() {
this.CheckListForm = this.fb.group({
name: ['', Validators.required],
EmploymentType: ['', Validators.required],
HRMS: [''],
CompanyName:[''],
questions: this.fb.array([])
})
}
get questions(): FormArray {
return this.CheckListForm.get('questions') as FormArray;
}
setquestions(questions: Questions[]) {
const QuestionsFGs = questions.map(questions => this.fb.group(questions));
const QuestionsFormArray = this.fb.array(QuestionsFGs);
this.CheckListForm.setControl('questions', QuestionsFormArray);
}
As usual, as there is no response from anyone, I will post the answer which i have painstakingly solved.
You cannot validate form-controls through the method i have performed(see my codes). The reason is because the form-controls already hold data and is not empty in the first place. Hence, validation.required will not work.
A solution will be to instead push empty form-controls for each item in the array of the data model.
Validation.required will then work for this case.
I'm trying to create a simple Aurelia reusable widget that encapsulates both a label and a text input field. The idea is to create a library of these reusable UI widgets to make it easier to compose screens and forms - perhaps taking some learnings from "Angular Formly".
text-field.html template:
<template>
<div class="form-group">
<label for.bind="name">${label}</label>
<input type="text" value.two-way="value" class="form-control" id.one-way="name" placeholder.bind="placeHolder">
</div>
</template>
text-field.js view model:
import {bindable} from 'aurelia-framework';
export class TextField {
#bindable name = '';
#bindable value = '';
#bindable label = '';
#bindable placeHolder = '';
}
client.html template snippet (showing usage of text-field):
<text-field name="firstName" value.two-way="model.firstName" label="First Name" placeHolder="Enter first name"></text-field>
<text-field name="lastName" value.two-way="model.lastName" label="Last Name" placeHolder="Enter last name"></text-field>
client.js view model (showing usage of text-field):
class ClientModel {
firstName = 'Johnny';
lastName = null;
}
export class Client{
heading = 'Edit Client';
model = new ClientModel();
submit(){
alert(`Welcome, ${this.model.firstName}!`);
}
}
QUESTION:
When the final HTML is generated, the attributes are "doubled up" by for example having both the id.one-way="name" AND id="firstName" (see below) - why is this and is there a better way to do this entire reusable text field control?:
<input type="text" value.two-way="value" class="form-control au-target" id.one-way="name" placeholder.bind="placeHolder" id="firstName" placeholder="">
That's normal. Same as if you do style.bind="expression" on a div and expression has display:block. You will end up with <div style.bind="expression" style="display:block"/>. The browser ignores style.bind because it is not a known html attribute. You can just ignore the Aurelia one.