Detecting changes in lit sub-properties - lit

I've made a simplified reduction of the issue I'm having with lit. Some of the state of my component is held in an object. I'd like to be able to detect and react to a change in a sub-property with some fine control. Here's the example:
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
#customElement("my-app")
export class App extends LitElement {
#state({
hasChanged(newVal: any, oldVal: any) {
console.log(`[hasChanged] new fruit is: ${newVal?.fruit}`);
console.log(`[hasChanged] old fruit is: ${oldVal?.fruit}`);
return true;
}})
data = {
fruit: "apples",
weather: "rain",
};
constructor() {
super();
setTimeout(() => {
this.data.fruit = "bananas";
this.data = { ...this.data };
}, 3000);
setTimeout(() => {
this.data.weather = "snow";
this.data = { ...this.data };
}, 6000);
}
render() {
return html`
Weather: ${this.data.weather} Fruit: ${this.data.fruit}
`;
}
shouldUpdate(changedProperties: any) {
// I only want to update if the weather is changing, not the fruit
console.log(
`new weather is: ${changedProperties.get("data")?.weather}`
);
console.log(`current weather is: ${this.data.weather}`);
console.log(`new fruit is: ${changedProperties.get("data")?.fruit}`);
console.log(`current fruit is: ${this.data.fruit}`);
console.log("");
if (changedProperties.get("data")?.weather !== this.data.weather) {
return true;
} else {
return false;
}
}
}
When shouldUpdate fires, the component's evaluation of the sub-property values has already updated. So I can't compare changedProperties.get("data")?.weather with this.data.weather to see if it has changed.
[Update] at michaPau's suggestion I looked into the hasChanged method - unfortunately this also gives back the same values for oldVal and newVal when a change is triggered.
Does anyone know a new approach or fix? It works as expected if I split that state object into two separate state variables, but objects and properties works so much better in keeping code readable. Passing more than 4 or 5 properties into a component starts getting messy, in my opinion.

I don't know exactly, what you are doing wrong, but your code seems to work actually, but yes as others mentioned, you should use the spread operator together with the assignment operation:
<script type="module">
import {
LitElement,
html,
css
} from "https://unpkg.com/lit-element/lit-element.js?module";
class MyContainer extends LitElement {
static get styles() {
return css`
.wrapper {
min-height: 100px;
min-width: 50%;
margin: 5em;
margin-top: 0;
padding: 10px;
background-color: lightblue;
}
`;
}
render() {
return html`
<div class="wrapper">
<slot></slot>
</div>
`;
}
}
class MyItem extends LitElement {
static properties = {
data: {
state: true,
hasChanged: (newVal, oldVal) => {
console.log(`[hasChanged] new fruit is: ${newVal?.fruit}`);
console.log(`[hasChanged] old fruit is: ${oldVal?.fruit}`);
return true;
}
}
};
constructor() {
super();
this.data = {
fruit: "apples",
weather: "rain"
};
setTimeout(() => {
this.data = { ...this.data, fruit: 'bananas' };
}, 3000);
setTimeout(() => {
this.data = { ...this.data, weather: "snow" };
}, 6000);
}
render() {
return html`
<div style="background-color: ${this.getBackgroundColor()}">
Weather: ${this.data.weather} Fruit: ${this.data.fruit}
</div>
`;
}
getBackgroundColor() {
if(this.data.weather === 'snow') return 'white';
else if(this.data.fruit === 'bananas') return 'yellow';
return 'blue';
}
shouldUpdate(changedProperties) {
// I only want to update if the weather is changing, not the fruit
console.log(`new weather is: ${changedProperties.get("data")?.weather}`);
console.log(`current weather is: ${this.data.weather}`);
console.log(`new fruit is: ${changedProperties.get("data")?.fruit}`);
console.log(`current fruit is: ${this.data.fruit}`);
console.log("");
if (changedProperties.get("data")?.weather !== this.data.weather) {
return true;
} else {
return false;
}
}
}
customElements.define("my-container", MyContainer);
customElements.define("my-item", MyItem);
</script>
<my-container>
<my-item></my-item>
</my-container>
If you want to see also the change to yellow change your code in the shouldUpdate function to return true if changedProperties.get("data") is not undefined.

Related

Firestore get array from collection with id's to use in Polymer dom-repeat

im trying to use Firestore with Polymer, I obtain an array to send it to polymer in a dom-repeat like this:
var query=db.collection("operaciones");
db.collection("operaciones")
.onSnapshot((querySnapshot) => {
querySnapshot.forEach(function(doc) {
});
that.operacionesPorCliente=Array.from(querySnapshot.docs.map(doc=>doc.data()));
});
console.log (that.operacionesPorCliente); // this works but the ID doesnt exist here....
}
that works but that array doesnt contain the id from firestore, the problem is that I need that ID to update the data :( but it isn't in the array
Hope I explain my self, any help?
I make a Polymer Element (Polymer 3) to keep Firebase Firestore data synchronized. This component has a dom-repeat template element, to show the always-fresh collection data.
I think this will answer your question
import {html, PolymerElement} from '#polymer/polymer/polymer-element.js';
import {} from '#polymer/polymer/lib/elements/dom-repeat.js';
/**
* #customElement
* #polymer
*/
class FirebaseFirestoreApp extends PolymerElement {
static get template() {
return html`
<style>
:host {
display: block;
}
</style>
<h1>Firestore test</h1>
<template is="dom-repeat" items="[[elems]]">
<p>[[item.$id]] - [[item.name]]</p>
</template>
`;
}
static get properties() {
return {
elems: {
type: Array,
value: function() {
return [];
}
}
};
}
ready() {
super.ready();
var db = firebase.firestore();
const settings = {timestampsInSnapshots: true};
db.settings(settings);
db.collection("operaciones").onSnapshot((querySnapshot) => {
querySnapshot.docChanges().forEach((change) => {
if (change.type === "added") {
let newElem = this.makeElem(change);
this.push('elems', newElem);
}
if (change.type === "modified") {
let modifiedElement = this.makeElem(change);
let index = this.getElemIndex(change.doc.id);
this.set(`elems.${index}`, modifiedElement);
}
if (change.type === "removed") {
let deletedElement = this.getElemIndex(change.doc.id);
this.splice('elems', deletedElement, 1);
}
});
});
}
makeElem(change) {
let data = change.doc.data();
data.$id = change.doc.id;
return data;
}
getElemIndex(id) {
let index = this.elems.findIndex((elem) => {
if(elem.$id == id) {
return true;
}
});
return index;
}
}
window.customElements.define('firebase-firestore-app', FirebaseFirestoreApp);
The sync systems should works with all kind of Firebase Firestore collections, but the template dom-repeat suposses there is a property called "name" in the objects inside the collection.
So, the collection in the Firebase console looks like that.
Firebase console screenshot
I suggest you use Polymerfire Polymerfire that are polymer components for Firebase, but if you want to do it in javascript, can get the id directly in the doc: doc.id().

On refresh react application I need to get from componentWillReceiveProps values for input text

I have the following problem and I really need help on that.
export class DeviceEdit extends React.PureComponent<Props> {
constructor(props) {
super(props);
this.state = {
value: ''
};
this.handleChange = this.handleChange.bind(this);
}
componentDidMount() {
let data = this.props.devices.data.find(device => device.id ===
`${deviceID}`) || {};
this.setState({ value: data.name })
}
componentWillMount() {
let data = this.props.devices.data.find(device => device.id ===
`${deviceID}`) || {};
this.setState({ value: data.name })
}
componentWillReceiveProps(newProps) {
let data = newProps.devices.data.find(device => device.id ===
`${deviceID}`) || {};
this.setState({ value: data.name })
}
handleChange(event) {
this.setState({value: event.target.value});
}
render() {
const { error } = this.props;
return (
<FormLabel>Internal ID</FormLabel>
<input type="text" defaultValue={this.state.value} onChange= .
{this.handleChange} />
</Form.Label>)
}
}
So what I want is that when I refresh the page, I want to get the the this.state.value on my input.. which in this case I am not able to do that. So I would like to know what I am doing wrong here. If I set it on value on the input I did get what I want, but then I have an warning like that:
A component is changing an uncontrolled input of type checkbox to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component
What can I do?
Actually, you should handle changes there and you can use just value on the input field instead of defaultValue.
For example:
export class AdminDeviceEdit extends React.PureComponent<Props> {
constructor(props) {
super(props);
this.state = {
value: '',
// if it comes from props by default
// you can use, if not just leave as it is
value: props.value
};
}
handleChange = e => {
this.setState({value: e.target.value});
}
render() {
const { error } = this.props;
return (
<form>
<FormLabel>Internal ID</FormLabel>
<input type="text" value={this.state.value} onChange={this.handleChange} />
</form>
)
}
}
Hope it will helps.
So from what I understand you want to make controlled input but use props.value as a default value. What if you do:
export class AdminDeviceEdit extends React.PureComponent<Props> {
constructor(props) {
super(props);
this.state = {
value: props.value,
};
this.handleChange = this.handleChange.bind(this);
}
componentWillReceiveProps(newProps) {
if(this.props.value !== newProps.value) {
this.setState({ value: newProps.value }) // reset input value
}
}
handleChange(event) {
this.setState({value: event.target.value});
}
render() {
const { error } = this.props;
return (
<FormLabel>Internal ID</FormLabel>
<input type="text" value={this.state.value} onChange={this.handleChange} />
</Form.Label>)
}
}
Certainly get rid of componentWillMount and componentDidMount. You don't need them here.

React Form validation displaying error

I am using React-Validation-Mixin together with Joi and Joi-Validation-Strategy to do some validations on a React Step/Wizard Form.
I have a parent FormStart Element that receives the state of its FormStep children through props.
The validation correctly signals that the input is required, but when I write a correct number in the browser (5 numbers as in PLZ/ZIP-Code), it will still signal that the input is invalid, even though the zip state shows a correct 5-digit number, so the next button never takes me to the next Form step.
class FormStart extends Component {
constructor(props) {
super(props);
this.state = {
step: 1,
zip: ""
}
this.goToNext = this.goToNext.bind(this);
}
goToNext() {
const { step } = this.state;
if (step !== 10) {
this.setState({ step: step + 1 });
if (step == 9) {
const values = {
zip: this.state.zip,
};
console.log(values);
// submit `values` to the server here.
}
}
}
handleChange(field) {
return (evt) => this.setState({ [field]: evt.target.value });
}
render(){
switch (this.state.step) {
case 1:
return <FormButton
onSubmit={this.goToNext}
/>;
//omitting the other 8 cases
case 9:
return <FormStep7
onSubmit={this.goToNext}
zip={this.state.zip}
onZipChange={this.handleChange('zip')}
/>;
case 10:
return <FormSuccess/>;
}
}
}
export default FormStart;
The React console shows that the zip state is correctly changed, and the Validation object also receives the same correct 5-digit zip and still holds the correct value onBlur.
class FormStep7 extends Component {
constructor(props) {
super(props);
this.validatorTypes = {
PLZ: Joi.number().min(5).max(5).required().label('PLZ').options({
language: {
number: {
base: 'wird benötigt',
min: 'muss {{limit}} Nummern enthalten',
max: 'muss {{limit}} Nummern enthalten'
}
}
})
};
this.getValidatorData = this.getValidatorData.bind(this);
this.getClasses = this.getClasses.bind(this);
this.renderHelpText = this.renderHelpText.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
getValidatorData() {
return {
PLZ: this.props.zip
};
}
getClasses(field) {
return classnames({
'form-control': true,
'has-error': !this.props.isValid(field)
});
}
renderHelpText(message) {
return (
<span className='help-block'>{message}</span>
);
}
handleSubmit(evt) {
evt.preventDefault();
const onValidate = (error) => {
if (error) {
//form has errors; do not submit
} else {
this.props.onSubmit();
}
};
this.props.validate(onValidate);
}
render() {
return (
<form role="form" onSubmit={this.handleSubmit}>
<div className='row'>
<div className="col-md-10 col-md-offset-1">
<div className='form-group'>
<label htmlFor="zip">
Einsatzort
</label>
<br />
<input className={this.getClasses('PLZ')} id="PLZ" placeholder="Meine PLZ" type="text" onChange={this.props.onZipChange} onBlur={this.props.handleValidation('PLZ')} value={this.props.zip} />
{this.renderHelpText(this.props.getValidationMessages('PLZ'))}
</div>
</div>
</div>
<div className='row'>
<div className="col-md-10 col-md-offset-1">
<button className="btn btn-green btn-block">Next</button>
</div>
</div>
</div>
</form>
);
}
}
FormStep7.propTypes = {
errors: PropTypes.object,
validate: PropTypes.func,
isValid: PropTypes.func,
handleValidation: PropTypes.func,
getValidationMessages: PropTypes.func,
clearValidations: PropTypes.func
};
export default validation(strategy)(FormStep7);
What am I doing wrong?
I found out that the issue was on Joi.number(). I changed the validation to match a Regex String pattern and then it worked.
this.validatorTypes = {
PLZ: Joi.string().regex(/^[0-9]{5}$/).label('PLZ').options({
language: {
string: {
regex: {
base: "mit 5 Nummern wird benötigt"
}
}
}
})
};

Setting state in react. Is there a better way to write this without warning errors?

I am working on a registration form on react. I am a bit stuck with the validation part of it.
As of now I am getting the following warnings four times on the console: "warning Do not mutate state directly. Use setState() react/no-direct-mutation-state."
I am guessing the reason I am getting these errors is because of statements like these "this.state.errors.firstName = "First name must be at least 2 characters.";" and like this"this.state.errors = {};" in my code.
However, I do not know how to make this better and eliminate the warnings. If you can provide a better way for me to do this that would be awesome. Any help will be highly appreciated. Thanks so much in advance!
import React, { Component } from 'react';
import {withRouter} from "react-router-dom";
import HeaderPage from './HeaderPage';
import Logo from './Logo';
import RegistrationForm from './RegistrationForm';
import axios from 'axios';
class Registration extends Component {
mixins: [
Router.Navigation
];
constructor(props) {
super(props);
this.state = {
firstName:'',
lastName:'',
email:'',
errors:{},
helpText: '',
helpUrl: '',
nextLink:''
};
this.setUserState = this.setUserState.bind(this);
this.registrationFormIsValid = this.registrationFormIsValid.bind(this);
this.saveUser = this.saveUser.bind(this);
}
setUserState(e){
const target = e.target;
const value = target.value;
const name = target.name;
this.setState({[name]: value});
//delete this line
console.log(this.state[name]);
}
registrationFormIsValid(){
var formIsValid = true;
this.state.errors = {};
//validate first name
if(this.state.firstName.length < 2){
this.state.errors.firstName = "First name must be at least 2 characters.";
formIsValid = false;
}
//validate last name
if(this.state.lastName.length < 2){
this.state.errors.lastName = "Last name must be at least 2 characters.";
formIsValid = false;
}
//validate email
if(this.state.email.length < 2){
this.state.errors.email = "Email must be at least 2 characters.";
formIsValid = false;
}
this.setState({errors : this.state.errors});
return formIsValid;
}
saveUser(e, { history }){
e.preventDefault();
// const errorWrappers = document.getElementsByClassName('input');
// for (var i=0; i < errorWrappers.length; i++) {
// const isError= errorWrappers[i].innerHTML;
// if (isError.length > 0){
// errorWrappers[i].previousSibling.className = "error-input"
// }
// }
if(!this.registrationFormIsValid()){
return;
}
const values = {
firstName: this.state.firstName,
lastName: this.state.lastName,
email: this.state.email,
password: this.state.password,
phone: this.state.phone,
address: this.state.address,
dob: this.state.birthday
}
if (this.props.userRole === 'instructor'){
axios.post(`/instructors`, values)
.then((response)=> {
//delete this line
console.log(response);
})
.catch((error) => {
console.log(error + 'something went wrooooong');
});
this.props.history.push("/success-instructor");
}else{
axios.post(`/students`, values)
.then((response)=> {
//delete this line
console.log(response);
})
.catch((error) => {
console.log(error + 'something went wrooooong');
});
if (this.props.parent === "false"){
this.props.history.push("/success-student");
}else{
this.props.history.push("/success-parent");
}
}
}
//end of validation
render() {
return (
<div className="Registration">
<div className="container menu buttons">
<HeaderPage/>
</div>
<div className="page container narrow">
<div className="cover-content">
<Logo/>
<div className="container">
<h2 className="page-title">{this.props.title}</h2>
<a className="helpLink" href={this.props.helpUrl}>{this.props.helpText}</a>
<div className="main-content background-white">
<RegistrationForm
userRole={this.props.userRole}
onChange={this.setUserState}
onSave={this.saveUser}
errors={this.state.errors}
/>
<br/>
<br/>
<br/>
</div>
</div>
</div>
</div>
</div>
);
}
}
export default withRouter(Registration);
Instead of
this.state.errors = {};
and
this.state.errors.lastName = "Last name must be at least 2 characters.";
use
this.setState({errors = {}});
this.setState({ errors: { lastName: "Last name must be at least 2 characters." } });
You need to avoid directly mutating the state.
The Warning itself answers the question. Please read the React Doc
carefully.
"warning Do not mutate state directly. Use setState()
react/no-direct-mutation-state."
Do not mutate state
Don't ever have code that directly changes state. Instead, create new object and change it. After you are done with changes update state with setState.
Instead of:
this.state.errors.someError1="e1";
this.state.errors.someError2="e2";
do this:
this.errorsObject=Object.assign({},this.state.errors,{someError1:"e1",someError2:"e2"};
and in the end:
this.setState({
errors:this.errorsObject
});
Object.assign lets us merge one object's properties into another one, replacing values of properties with matching names. We can use this to copy an object's values without altering the existing one.

Dynamically include files (components) and dynamically inject those components

Looking around the next I could not find the answer: How do I dynamicly include a file, based on prop change per say: here some sudo code to intrastate what I'm trying to do!
class Test extends React.Component {
constructor(props){
super(props)
this.state = { componentIncluded: false }
includeFile() {
require(this.props.componetFileDir) // e.g. ./file/dir/comp.js
this.setState({ componentIncluded: true });
}
render() {
return(
<div className="card">
<button onClick={this.includeFile}> Load File </button>
{ this.state.componentIncluded &&
<this.state.customComponent />
}
</div>
)
}
}
so this.props.componetFileDir has access to the file dir, but I need to dynamically include it, and can't really do require() as its seems to running before the action onClick get called. Any help would be great.
Em, Your code looks a bit wrong to me. So I created a separate demo for dynamic inject components.
While in different situation you can use different React lifecycle functions to inject your component. Like componentWillReceiveProps or componentWillUpdate.
componentDidMount() {
// dynamically inject a Button component.
System.import('../../../components/Button')
.then((component) => {
// update the state to render the component.
this.setState({
component: component.default,
});
});
}
render() {
let Button = null;
if (this.state.component !== null) {
Button = this.state.component;
}
return (
<div>
{ this.state.component !== null ? <Button>OK</Button> : false }
</div>
);
}
After you edited your code, it should be something similar to below:
class Test extends React.Component {
constructor(props){
super(props)
this.state = { customComponent: null }
this.includeFile = this.includeFile.bind(this);
}
includeFile() {
System.import(this.props.componetFileDir)
.then((component) => {
this.setState({ customComponent: component.default });
});
}
render() {
return(
<div className="card">
<button onClick={this.includeFile}> Load File </button>
{
this.state.customComponent
}
</div>
)
}
}