Are these bugs in lit-html? - lit-html

In lit-html 1.0.0-rc.2, I have the following template, which does not work correctly. So, I suppose I'm doing something wrong. Or is this just a bug in lit-html?
import { html } from './node_modules/lit-html/lit-html.js';
import css from './css.js';
export default function template(c) {
console.log('props', c.text, c.selected, c.checkbox, c.radioButton);
const changeText = c.changeText.bind(c);
return html`
${css()}
<form>
<div>input cannot be updated programatically</div>
<input type="text" value="${c.text}" #input="${changeText}"/>
<div>select cannot be set/changed programatically</div>
<select #change="${ev => {c.selected = ev.currentTarget.value; console.log('value set to', ev.currentTarget.value)}}">
<option value="" ?selected="${c.selcted === ''}">Select</option>
<option value="1" ?selected="${c.selcted === '1'}">1</option>
<option value="2" ?selected="${c.selcted === '2'}">2</option>
<option value="3" ?selected="${c.selcted === '3'}">3</option>
<option value="4" ?selected="${c.selcted === '4'}">4</option>
</select>
<div>checkbox cannot be updated programatically</div>
<input
type="checkbox"
#change="${(ev) => {c.checkbox = ev.currentTarget.value; console.log('checkbox value:', ev.currentTarget.value)}}"
?checked="${c.checkbox === 'on'}"
/>
<div>radio buttons cannot be updated programatically</div>
<input
name="radio"
type="radio"
value="1"
#change="${ev => {c.radioButton = '1'; console.log('radio button value: ', ev.currentTarget.value)}}"
?checked="${c.radioButton === '1'}"/>
<label>1</label>
<input
name="radio"
type="radio"
value="2"
#change="${ev => {c.radioButton = '2'; console.log('radio button value: ', ev.currentTarget.value)}}"
?checked="${c.radioButton === '2'}"/>
<label>2</label>
<input
name="radio"
type="radio"
value="3"
#change="${ev => {c.radioButton = '3'; console.log('radio button value: ', ev.currentTarget.value)}}"
?checked="${c.radioButton === '3'}"/>
<label>3</label>
</form>
`;
}
It is populated/controlled by the following web component:
import { render } from './node_modules/lit-html/lit-html.js';
import template from './template.js';
class MyForm extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.text = 'foo';
this.selected = '2';
this.checkbox = 'on';
this.radioButton = '1';
}
attributeChangedCallback(name, oVal, nVal) {
}
connectedCallback() {
this.render();
setInterval(() => {
this.text = 'foobar';
this.selected = '3';
this.checkbox = 'off';
this.radioButton = '2';
this.render();
}, 2000);
}
disconnectedCallback() {
}
changeText(ev) {
const { value } = ev.currentTarget;
this.text = value;
this.render();
}
render() {
render(template(this), this.shadowRoot);
}
static get observedAttributes() {
return ['']
}
}
customElements.get('my-form') || customElements.define('my-form', MyForm);
export { MyForm }
Note the web component attempts to set the value of various inputs on first render. Thereafter, it attempts to set them again using setInterval. setInterval is used solely to show how the web component is attempting to update the template.
In the case of the select, an option cannot be set programatically. And in the case of each of the other input elements, once selected in the UI cannot be updated programatically.

I don't think it's a bug in Lit, though you're using it in quite a unique way.
In the case of your <select> the problem is that you're setting c.selected but then checking c.selcted in each <option>.

Related

Can't clear form/state after input in React.js

I have a form which ultimately will be used as the UI to make some API calls to Open weather map.
Right now when I submit the a zip code in the input field, upon submission [object Object] propagates the field like in the screen shot below.
The call to the API is working as I am getting the JSON for the correct zip code...
But shouldn't this in the handleSubmit take care of everything i.e. using Object.assign to create new state and then using form.zipcode.value = ''; to clear out the input?
Thanks in advance!!
handleSubmit(event) {
event.preventDefault();
var form = document.forms.weatherApp;
api.getWeatherByZip(this.state.zipcode).then(
function(zip) {
console.log('zip', zip);
this.setState(function() {
return {
zipcode: Object.assign({}, zip),
};
});
}.bind(this)
);
form.zipcode.value = '';
}
I have enclosed all of the component's code here.
import React, { Component } from 'react';
import * as api from '../utils/api';
import '../scss/app.scss';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
zipcode: [],
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({
zipcode: event.target.value,
});
}
handleSubmit(event) {
event.preventDefault();
var form = document.forms.weatherApp;
api.getWeatherByZip(this.state.zipcode).then(
function(zip) {
console.log('zip', zip);
this.setState(function() {
return {
zipcode: Object.assign({}, zip),
};
});
}.bind(this)
);
form.zipcode.value = '';
}
render() {
return (
<div className="container">
<form name="weatherApp" onSubmit={this.handleSubmit}>
<h2>Open Weather App</h2>
<div className="row">
<div className="one-half column">
<label htmlFor="insertMode">Insert your location</label>
<input
name="zipcode"
className="u-full-width"
placeholder="please enter your zipcode"
type="text"
autoComplete="off"
value={this.state.zipcode}
onChange={this.handleChange}
/>
</div>
<div className="one-half column">
<label htmlFor="showMin">show minimum</label>
<input type="checkbox" />
<label htmlFor="showMax">show maximum</label>
<input type="checkbox" />
<label htmlFor="showMean">show mean</label>
<input type="checkbox" />
</div>
</div>
<div className="row">
<div className="two-half column">
<input type="submit" value="Submit" />
</div>
</div>
</form>
</div>
);
}
}
You should let react manage the changes to the DOM rather that editing it manually. As the value of your input field is already bound to this.state.zipcode to reset it just invoke this.setState({zipcode: ''}) instead of form.zipcode.value='';.

React updating state in two input fields from form submission

I am trying to make a simple contact form using React. Eventually I will send the data collected from the state to a database, but for right now I am trying to just get it to console log the correct values.
Right now, the email field overrides the name field and when I console log both states, name shows up and email is undefined. Here is my React Component
import React, { Component, PropTypes } from 'react';
import ContactData from '../data/ContactData.js';
class FormContact extends Component {
constructor(props) {
super(props)
this.state = {
name: '',
email: '',
textArea: ''
}
}
handleChange(event) {
event.preventDefault();
this.setState({
name: event.target.value,
email: event.target.email
})
}
handleSubmit(event) {
event.preventDefault();
console.log(this.state.name + ' ' + this.state.email);
}
render() {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<label> Name:
<input type="text" placeholder="Name" value={this.state.name} onChange={this.handleChange.bind(this)} />
</label><br />
<label> Email:
<input type="text" placeholder="Email" value={this.state.email} onChange={this.handleChange.bind(this)}/>
</label><br />
<input className="btn btn-primary" type="submit" value="Submit" />
</form>
)
}
}
FormContact.PropTypes = {
subName: PropTypes.string,
subEmail: PropTypes.string
}
FormContact.defaultProps = {
subName: 'Sam',
subEmail: ''
}
class Contact extends Component {
render() {
return (
<div>
<h1>CONTACT PAGE</h1>
<FormContact />
</div>
)
}
}
export default Contact;
If I understand what you want, you could do it as follows :
Add an empty object in your state for the form values
formValues: {}
Add the name attribute to your fields
<input name="name" .... />
<input name="email" .... />
then depending on that name update your state in handleChange function
let formValues = this.state.formValues;
let name = event.target.name; // Field name
let value = event.target.value; // Field value
formValues[name] = value;
this.setState({formValues})
And if the values go one level deeper, you could use
value={this.state.formValues["name"]} instead of value={this.state.name} - where name is the value of the name attribute of your input field
Thus, everything together should be as follows :
class Test extends React.Component {
constructor(props) {
super(props)
this.state = {
formValues: {}
}
}
handleChange(event) {
event.preventDefault();
let formValues = this.state.formValues;
let name = event.target.name;
let value = event.target.value;
formValues[name] = value;
this.setState({formValues})
}
handleSubmit(event) {
event.preventDefault();
console.log(this.state.formValues);
}
render(){
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<label> Name:
<input type="text" name="name" placeholder="Name" value={this.state.formValues["name"]} onChange={this.handleChange.bind(this)} />
</label><br />
<label> Email:
<input type="text" name="email" placeholder="Email" value={this.state.formValues["email"]} onChange={this.handleChange.bind(this)}/>
</label><br />
<input className="btn btn-primary" type="submit" value="Submit" />
</form>
)
}
}
React.render(<Test />, document.getElementById('container'));
Here is fiddle.
Hope this helps.
The reference to event.target.email does not exist on the event element. The value of a text input from an inline-event handler would be event.target.value for both email and name. The quick solution is to create a separate handler for each input:
handleChangeName(event) {
event.preventDefault();
this.setState({ name: event.target.value }); //<-- both use the same reference
} // to get the contextual value
handleChangeEmail(event) { // from the inputs v
event.preventDefault(); // |
this.setState({ email: event.target.value }); //<--------------------
}

Angular 2 - Form validation for warnings/hints

I'm trying to add form validations which don't invalidate the form. The validation should only appear as warnings.
E.g. an age validation. An age greater than 90 shows a warning and an age greater than 120 shows an error.
I've tried it with two FormGroups on a form and two [formControl]s on the input field. Only the first [formControl] is used.
Is it possible to use Angulars form validation for this kind of validation? Which approach is the way to go?
I have done it by creating custom validator, which always return null. Also this validator creates additional property warnings. Then just simple check this property from your view.
export interface AbstractControlWarn extends AbstractControl { warnings: any; }
export function tooBigAgeWarning(c: AbstractControlWarn) {
if (!c.value) { return null; }
let val = +c.value;
c.warnings = val > 90 && val <= 120 ? { tooBigAge: {val} } : null;
return null;
}
export function impossibleAgeValidator(c: AbstractControl) {
if (tooBigAgeWarning(c) !== null) { return null; }
let val = +c.value;
return val > 120 ? { impossibleAge: {val} } : null;
}
#Component({
selector: 'my-app',
template: `
<div [formGroup]="form">
Age: <input formControlName="age"/>
<div *ngIf="age.errors?.required" [hidden]="age.pristine">
Error! Age is required
</div>
<div *ngIf="age.errors?.impossibleAge" [hidden]="age.pristine">
Error! Age is greater then 120
</div>
<div *ngIf="age.warnings?.tooBigAge" [hidden]="age.pristine">
Warning! Age is greater then 90
</div>
<p><button type=button [disabled]="!form.valid">Send</button>
</div>
`,
})
export class App {
age: FormControl;
constructor(private _fb: FormBuilder) { }
ngOnInit() {
this.form = this._fb.group({
age: ['', [
Validators.required,
tooBigAgeWarning,
impossibleAgeValidator]]
})
this.age = this.form.get("age");
}
}
Example: https://plnkr.co/edit/me0pHePkcM5xPQ7nzJwZ?p=preview
The accepted answer from Sergey Voronezhskiy works perfect in development mode but if you try build in --prod mode you will get this error.
... Property 'warnings' does not exist on type 'FormControl'.
In order to fix this error I did adjustments to the original code (App class), basically to fix loosely typed variables. This is the new version:
export class FormControlWarn extends FormControl { warnings: any; }
export function tooBigAgeWarning(c: FormControlWarn ) {
if (!c.value) { return null; }
let val = +c.value;
c.warnings = val > 90 && val <= 120 ? { tooBigAge: {val} } : null;
return null;
}
export function impossibleAgeValidator(c: AbstractControl) {
if (tooBigAgeWarning(c) !== null) { return null; }
let val = +c.value;
return val > 120 ? { impossibleAge: {val} } : null;
}
#Component({
selector: 'my-app',
template: `
<div [formGroup]="form">
Age: <input formControlName="age"/>
<div *ngIf="age.errors?.required" [hidden]="age.pristine">
Error! Age is required
</div>
<div *ngIf="age.errors?.impossibleAge" [hidden]="age.pristine">
Error! Age is greater then 120
</div>
<div *ngIf="age.warnings?.tooBigAge" [hidden]="age.pristine">
Warning! Age is greater then 90
</div>
<p><button type=button [disabled]="!form.valid">Send</button>
</div>
`,
})
export class App {
get age(): FormControlWarn{
return <FormControlWarn>this.form.get("age");
}
constructor(private _fb: FormBuilder) { }
ngOnInit() {
this.form = this._fb.group({
age: new FormControlWarn('', [
Validators.required,
tooBigAgeWarning,
impossibleAgeValidator])
});
}
}
This is probably how I would have done it.
<form #form="ngForm" (ngSubmit)="save()">
<input formControlName="controlName">
<span *ngIf="form.pristine && form.controls.controlName.value > 90 && form.controls.controlName.value < 120">
Warning: Age limit is high..
</span>
<span *ngIf="form.pristine && form.controls.controlName.value > 120">
Error: Age limit exceeded..
</span>
<form>
Ok
its can be easy by angular.io for form validation hinting you can read documents on https://angular.io/docs/ts/latest/cookbook/form-validation.html
but the similar way that can help you better may be in my mind.
first we create a abstract class named Form it contains some common function and properties.
import {FormGroup} from "#angular/forms";
export abstract class Form {
form: FormGroup;
protected abstract formErrors: Object;
protected abstract validationMessages: Object;
onValueChanged(data?: any) {
if (!this.form) { return; }
const form = this.form;
for (const field in this.formErrors) {
this.formErrors[field] = '';
const control = form.get(field);
if (control && control.dirty && !control.valid) {
const messages = this.validationMessages[field];
for (const key in control.errors) {
this.formErrors[field] = messages[key];
break;
}
}
}
}
}
then you should to create a form component for example named LoginComponent like below
import {Component, OnInit} from "#angular/core";
import {Form} from "./form";
import {Validators, FormBuilder} from "#angular/forms";
#Component({
templateUrl: '...'
})
export class LoginComponent extends Form implements OnInit {
protected formErrors = {
'email': '',
'password': ''
}
protected validationMessages = {
'email': {
'required': 'email required message',
'email': 'email validation message'
},
'password': {
'required': 'password required message',
'minlength': 'password min length message',
'maxlength': 'password max length message',
}
}
constructor(private _fb: FormBuilder) { }
ngOnInit() {
this.buildForm();
}
buildForm() {
this.form = this._fb.group({
'email': ['', [
Validators.required,
// emailValidator
]],
'password': ['', [
Validators.required,
Validators.minLength(8),
Validators.maxLength(30)
]]
});
this.form.valueChanges
.subscribe(data => this.onValueChanged(data));
this.onValueChanged(); //
}
}
first we should inject FormBuilder for reactive forms (do not forgot to import ReactiveFormModule in your main module) and then in [buildForm()] method we build a group of form on form property that was inherited from abstract class Form.
then in next we create a subscribe for form value changes and on value change we call [onValueChanged()] method.
in [onValueChanged()] method we check the fields of form has valid or not, if not we get the message from protected validationMessages property and show it in formErrors property.
then your template should be similar this
<div class="col-md-4 col-md-offset-4">
<form [formGroup]="form" novalidate>
<div class="form-group">
<label class="control-label" for="email">email</label>
<input type="email" formControlName="email" id="email" class="form-control" required>
<div class="help help-block" *ngIf="formErrors.email">
<p>{{ formErrors.email }}</p>
</div>
</div>
<div class="form-group">
<label class="control-label" for="password">password</label>
<input type="password" formControlName="password" id="password" class="form-control" required>
<div class="help help-block" *ngIf="formErrors.password">
<p>{{ formErrors.password }}</p>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-block btn-primary" [disabled]="!form.valid">login</button>
</div>
</form>
</div>
the template is so easy, inner you check the field has error or not if has the error bind.
for bootstrap you can do something else like below
<div class="form-group" [ngClass]="{'has-error': form.controls['email'].dirty && !form.controls['email'].valid, 'has-success': form.controls['email'].valid}">
<label class="control-label" for="email">email</label>
<input type="email"
formControlName="email"
id="email"
class="form-control"
required>
<div class="help help-block" *ngIf="formErrors.email">
<p>{{ formErrors.email }}</p>
</div>
</div>
UPDATE: I attempt to create a very simple but almost complete sample for you certainly you can develop it for wilder scale :
https://embed.plnkr.co/ExRUOtSrJV9VQfsRfkkJ/
but a little describe for that, you can create a custom validations like below
import {ValidatorFn, AbstractControl} from '#angular/forms';
function isEmptyInputValue(value: any) {
return value == null || typeof value === 'string' && value.length === 0;
}
export class MQValidators {
static age(max: number, validatorName: string = 'age'): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} => {
if (isEmptyInputValue(control.value)) return null;
const value = typeof control.value == 'number' ? control.value : parseInt(control.value);
if (isNaN(value)) return null;
if (value <= max) return null;
let result = {};
result[validatorName] = {valid: false};
return result;
}
}
}
this custom validator get a optional param named validatorName, this param cause you designate multi similar validation as in your example the formComponent should be like below :
buildForm () {
this.form = this._fb.group({
'age': ['', [
Validators.required,
Validators.pattern('[0-9]*'),
MQValidators.age(90, 'abnormalAge'),
MQValidators.age(120, 'incredibleAge')
]]
});
this.form.valueChanges
.subscribe(data => this.onValueChanged(data));
this.onValueChanged();
}
onValueChanged(data?: any): void {
if (!this.form) { return; }
const form = this.form;
for (const field in this.formErrors) {
this.formErrors[field] = '';
const control = form.get(field);
if (control && control.dirty && !control.valid) {
const messages = this.validationMessages[field];
for (const key in control.errors) {
this.formErrors[field] = messages[key];
}
}
}
}
formErrors = {
age: ''
}
validationMessages = {
'age': {
'required': 'age is required.',
'pattern': 'age should be integer.',
'abnormalAge': 'age higher than 90 is abnormal !!!',
'incredibleAge': 'age higher than 120 is incredible !!!'
}
}
Hope I helped.

Nested form state and one onChange function in React

I'm working on a form in React. I have nested state in MatchForm component and "bind" values from state with different inputs. I would like to have on onChange function which manages all cinput changes and pass it to state. Current onChange function works when I have only lineup inputs. But when I added other inputs I have no idea how to handle with it. How should I name inputs and how onChange function should look like to work with all inputs (without many ifs)?
Thank you in advance
class MatchForm extends React.Component {
constructor(props) {
super(props);
this.state = {
lineup: {
setter: '',
receiver1: '',
receiver2: '',
attacker: '',
blocker1: '',
blocker2: '',
libero: ''
},
distribution: {
receiver1: 20,
receiver2: 20,
attacker: 20,
blocker1: 20,
blocker2: 20
},
risk: 'normal',
default: false
};
this.onChange = this.onChange.bind(this);
}
onChange(e) {
this.setState({lineup: { ...this.state.lineup, [e.target.name]:e.target.value } })
}
render() {
return (
<div>
{
Object.keys(this.state.lineup).map((el,i) =>
<select
key={i}
name={el}
onChange={this.onChange}
value={this.state.lineup[el]}
>
<option value="" disabled>{el}</option>
<option value="Marian Noga">Marian Noga</option>
<option value="Janek Kowalski">Janek Kowalski</option>
</select>
)
}
{
Object.keys(this.state.distribution).map((el,i) =>
<Field
key={i}
field={el}
label={el}
type='number'
value={this.state.distribution[el]}
onChange={this.onChange} />
)
}
<select
name="risk"
onChange={this.onChange}
value={this.state.risk}
>
<option value="" disabled>risk</option>
<option value="safe">safe</option>
<option value="normal">normal</option>
<option value="risk">risk</option>
</select>
<label><input name="default" type="checkbox" value={this.state.default} onChange={this.onChange} /> Set as default lineup</label>
</div>
)
}
}
export default MatchForm
You can bind the onChange function with different values for each of your fields:
onChange={this.onChange.bind(this, 'lineup', el)}
Then if you write your onChange like this:
onChange(field, name, e) {
this.setState({[field]: { ...this.state[field], [name]:e.target.value } });
}
You should be able now to repeat that for each of your fields.
If it is just a controlled input, and no other UI logic, I do something as simple as this:
<input type="text"
name="email"
value={this.state.email}
onChange={e => this.setState({ email: e.target.value })}
/>
You could also try _.set for more complex use cases. Good thing is, the set function alone can be downloaded as an npm package
<input type="text" name="lineup.receiver1" onChange={handleChange} value={this.state.lineup.receiver1} />
import update from "lodash.set";
const handleChange = (ev) => { update(this.state, ev.target.name, ev.target.value ) }
_.set has more complex options for updating a nested object and can be used in our case with HTML name property of a field.

In meteor js ,In edit form i'm unable to display the dropdown box values which i stored during adding a form

I have a form that uses drop-down boxes to save information in mongo
Db. That works fine, but the problem comes when I am trying to edit
the information in the database.I used the same form (add form) to
edit the information. it pulls the values from the database and
displays the fields in the fields accordingly. However, I am having a
hard time figuring out how to populate the drop-down boxes with the
value from the database. Basically, I want the "option selected" tag
to be the database and be able to still have the rest of the options
to select from.in text box I am getting the text values from database
but in drop down I wont be able to get the value.
addmenu.html
<template name="addMenu">
<form class="addingMenus">
<p><input type="text" name="menuName" id="menuName" placeholder="Choose Label" value = {{menuName}}></p>
<p><input type="text" name="associatedPages" id="associatedPages" placeholder="Enter Associated Pages" value= {{associatedPages}}></p>
<p><input type="text" name="menuUrl" id="menuUrl" placeholder="Enter Page URl" value={{menuUrl}}></p>
<p>
<select id="level" >
<option name="parent" value="0" selected = {{rejected}}>parent level</option>
<option name="child" value="1" selected = {{accepted}}>child level</option>
</select>
<select id="childLevel" style="visibility:hidden">
{{#each parent}}
<option value = "{{this._id}}" selected = {{subMenu}} >{{this.menuName}}</option>
{{/each}}
</select>
</p>
<p>
<select id="publishStatus">
<option name="publish" value="true" selected="{{published}}">publish</option>
<option name="unpublish" value="false" selected="{{unpublished}}">unpublish</option>
</select>
</p>
<p><button type="button" class="save-button" id="{{task}}-save-button">SAVE</button></p>
</form>
</template>
addmenu.js:
Template.addMenu.events({
'click #add-menu-save-button': function (event,template) {
//event.preventDefault();
console.log(event);
var levelId = document.getElementById('level').value;
if (levelId == 1) {
parentId = document.getElementById('childLevel').value;
} else {
parentId = "null";
}
var publishStatus = document.getElementById('publishStatus').value;
//console.log(publishStatus);
let menuInsert = {
menuName: document.getElementById('menuName').value,
associatedPages: document.getElementById('associatedPages').value,
menuUrl: document.getElementById('menuUrl').value,
level: document.getElementById('level').value,
createdAt: new Date(),
publishStatus: publishStatus,
parentId: parentId
};
Meteor.call("addMenu", menuInsert, function (error, result) {
if(error) {
console.log("error in adding a menu");
} else {
alert("successfully entered in database");
Router.go('/administrator/admin');
}
});
},
'click #level': function (event, template) {
event.preventDefault();
console.log(document.getElementById('level').value);
if(document.getElementById('level').value == '1') {
document.getElementById("childLevel").style.visibility = "visible";
console.log("iam in session in level");
} else {
document.getElementById("childLevel").style.visibility = "hidden";
}
}
});
Template.addMenu.helpers({
parent: function () {
return menuDetails.find({level: "0"});
},
accepted: function (event) {
console.log(this.level);
if(this.level == "1") {
Session.set('submenu',this.parentId);
console.log(Session.get('submenu'));
return "selected";
}
},
rejected: function (event) {
if(this.level == "0") {
return "selected";
}
},
subMenu: function (event) {
var id = Session.get('submenu');
console.log(this._id);
if(id == this._id) {
return "selected";
}
},
published: function (event) {
if(this.publishStatus == true)
return "selected";
},
unpublished: function (event) {
if(this.publishStatus == false)
return "selected";
}
});
you need to modify your template like this.
<select id="childLevel" style="visibility:hidden">
{{#each parent}}
<option {{isSelected this.menuName}} value = "{{this._id}}">{{this.menuName}}</option>
{{/each}}
</select>
then in the helper you need to write this custom helper.
Template.addMenu.helpers({
isSelected: function(menuName){
return (menuName == 'your conditional value here') ? 'selected': '';
}
});