Handling form data in PreactJS - forms

I am porting a react app to preact. Handling data from a form on submit throws a TypeError: this.createIssue is not a function at Object.createTestIssue [as click] error.
The code is a follows:
class IssueList extends Component {
state = { issues: [] };
createIssue(newIssue) {
const updatedIssues = this.state.issues.slice();
newIssue.id = this.state.issues.length + 1;
updatedIssues.push(newIssue);
this.setState({ issues: updatedIssues });
}
createTestIssue() {
const issue = {
status: 'New',
owner: 'Pieta',
created: new Date(),
title: 'Completion date should be optional'
};
this.createIssue(issue);
}
render(props, state) {
return (
<div>
<h1>Issue Tracker</h1>
<IssueFilter />
<hr />
<IssueTable issues={state.issues} />
<hr />
<IssueAdd createIssue={this.createIssue} />
<hr />
<button onClick={this.createTestIssue}>Add Test</button>
</div>
);
}
}
export default IssueList;
I have tried to create a constructor and setting the state from inside the constructor, however there is a ReferenceError: state is not defined at new IssueList.
I have looked at linkState module that preact recommends for forms, however, I have not been able to set it up correctly. Do I pass an empty object and an object with the data that I want to be added to my array; something like:
render({}, { state }) {
...
}
But that does not allow me to access the state. Any help is appreciated.

In preact props and state are passed into the render function, as you have shown.
render(props, state) {
...
}
This allows you to use destructuring#MOZDocs to
render({}, { state }) {
...
}
What you have ^^ would not be beneficial, and might hide state behind another object. What you should have is:
render({}, { ...state }) {
...
}
OR in my opinion the best solution assuming state = {key: 'test', value: 1}:
render({}, { key, value }) {
...
}
If you do not wish to use anything from props you can either use the empty object as shown above or just have props, but then not use it.

Related

Vuetify TreeView + Drag and drop

I am trying to implement drag and drop on Vuetify Treeview and data table. It seems like it is not supported fully but a workaround is described in this thread. The workaround is however not complete. Perhaps the community would benefit if someone created a codepen or similar on this?
What confuses me is that the component DragDropSlot.vue is created but "drag-drop-slot" is used in the code. Also there is a "_.cloneDeep(this.tree)" call where _ is not defined. I assume it should be replaced by something. When I comment that out drag and drop does still not work. Probably missed something more like defining data. Not sure of correct data types. It seems to be based on react which I have not worked with. Have just started to learn vue and vuetify.
I'm open for any suggestion for how to solve this.
All the best
I use V-Treeview with Vue.Draggable (https://github.com/SortableJS/Vue.Draggable).
I use direct link.
<script src="//cdn.jsdelivr.net/npm/sortablejs#1.8.4/Sortable.min.js"/>
<script src="//cdnjs.cloudflare.com/ajax/libs/Vue.Draggable/2.20.0 vuedraggable.umd.min.js"/>
<v-treeview
:active.sync="active"
:items="users"
:search="search"
item-key="Id"
item-text="UserName"
item-children="Children"
:open.sync="open"
activatable
color="warning"
dense
transition
return-object
>
<template v-slot:label="{ item }">
<draggable :list="users" group="node" :id="item.Id" :data-parent="item.ParentId" #start="checkStart" #end="checkEnd" >
<label>
<i class="fas fa-user mr-3" />
<span id="item.id" >{{item.UserName}}</span>
</label>
</draggable>
Also I add ParentId property to item tree model:
{
Id:1,
UserName: "John Doe",
ParentId: null,
Children:[{Id:2, ParentId: 1,...}]
}
Then I use start and end events where I search parent start node from I drag the item and parent end node where I drop the item. When parent is null the item is a root.
new Vue({
el: '#app',
vuetify: new Vuetify(),
components: {
vuedraggable
},
data() {
return {
active: [],
open: [],
users: [],
selectedItems: [],
}
},
mounted: function () {
this.fetchUsers();
},
methods: {
findTreeItem: function (items, id) {
if (!items) {
return;
}
for (var i = 0; i < items.length; i++) {
var item = items[i];
// Test current object
if (item.Id === id) {
return item;
}
// Test children recursively
const child = this.findTreeItem(item.Children, id);
if (child) {
return child;
}
}
},
checkStart: function (evt) {
var self = this;
self.active = [];
self.active.push(self.findTreeItem(self.users, evt.from.id))
},
checkEnd: function (evt) {
var self = this;
var itemSelected = self.active[0];
var fromParent = itemSelected.ParentId ? self.findTreeItem(self.users, itemSelected.ParentId) : null;
var toParent = self.findTreeItem(self.users, evt.to.id);
var objFrom = fromParent ? fromParent.Children : self.users;
objFrom.splice(objFrom.indexOf(itemSelected), 1);
if (toParent.Id === itemSelected.Id) {
itemSelected.ParentId = null;
self.users.push(itemSelected);
}
else {
itemSelected.ParentId = toParent.Id;
toParent.Children.push(itemSelected);
}
self.saveUser(itemSelected);
// self.active = [];
return false;
},
fetchUsers: function () {
//load from api
},
saveUser: function (user) {
//save
},
},
computed: {
selected() {
if (!this.active.length) return undefined
return this.active[0];
},
}
})
Hope I help you.
IngD.
After some additional work I ended up with implementing Drag and Drop on top of vuetify tree view and data table using this library:
https://www.vuetoolbox.com/projects/vue-drag-drop
At first I looked at draggable and similar but realized it was always based on that you move an element from position A to position B. I needed more control. For example I wanted the element to disappear when dropping on some drop zones.
found this component.
https://vuejsexamples.com/vuetify-draggable-v-treeview-component/
I didn't try it myself (because it has too few options), but it looks working well in demo.
Anyways, just to try

Resetting forms in Polymer 2.x

I am trying to reset my form. What am I doing wrong? What is best-practice?
Here is my Plunk demo.
My problem on the demo is that connectedCallback() appears to fire continually (not just on initial load), thereby losing the value of savedItem by updating it to newItem on each update.
Here is the same issue on Github.
https://plnkr.co/edit/wRdXXws2UXl3VXrycqua?p=preview
my-demo.html
<base href="https://polygit.org/polymer+v2.0.0/shadycss+webcomponents+1.0.0/components/">
<link rel="import" href="polymer/polymer-element.html">
<link rel="import" href="paper-toggle-button/paper-toggle-button.html">
<dom-module id="my-demo">
<template>
<style>
:host > * {
margin-top: 40px;
font-size: 18px;
}
button.save {
color: white;
background-color: blue;
}
</style>
<paper-toggle-button checked="{{item.alice}}">Alice</paper-toggle-button>
<paper-toggle-button checked="{{item.bob}}">Bob</paper-toggle-button>
<paper-toggle-button checked="{{item.charlie}}">Charlie</paper-toggle-button>
<paper-toggle-button checked="{{item.dave}}">Dave</paper-toggle-button>
<button>Reset</button>
<button class="save" on-tap="_reset">Save</button>
</template>
<script>
class MyDemo extends Polymer.Element {
static get is() {
return 'my-demo';
}
static get properties() {
return {
item: {
type: Object,
notify: true,
value: () => {
return {
alice: false,
bob: false,
charlie: false,
dave: true,
};
},
},
savedItem: {
type: Object,
notify: true,
},
};
}
connectedCallback() {
super.connectedCallback();
this.set('savedItem', this.item);
}
static get observers() {
return [
'_itemChanged(item.*)',
];
}
_itemChanged(newItem) {
console.log('saved-item', this.savedItem);
console.log('new-item', newItem);
}
_reset() {
this.set('item', this.savedItem);
}
}
window.customElements.define(MyDemo.is, MyDemo);
</script>
</dom-module>
Edit
Steps to recreate the problem
Open the demo here.
Open your console.
Navigate in the Plunker to my-demo.html
Click one of the toggle switches.
Notice in the console, the savedItem property updates to the current item property.
Notice, this appears to be the result of the following code block.
connectedCallback() {
super.connectedCallback();
this.set('savedItem', this.item);
}
But how can this be? Because I thought connectedCallback() only fired once at initialization time?
tldr; The connectedCallback() isn't actually being called more than once in this case. savedItem and item are always the same object in your code because JavaScript passes objects by reference.
Object references
In the following:
connectedCallback() {
this.set('savedItem', this.item);
}
_reset() {
this.set('item', this.savedItem);
}
savedItem and item are both references to the same object. Calling this.set() does not automatically clone the operand (nor does the = operator).
One solution is to clone the object before assignment (using ES2017 object-spread operator):
connectedCallback() {
this.savedItem = {...this.item};
}
_reset() {
this.item = {...this.savedItem};
}
updated plunker
Best practice (or simpler reset method)
A simpler way to reset the form is to let iron-form handle the form's reset event, where it resets the form's named inputs to their initial values. This saves you from having to declare savedItem and no extra JavaScript to manage it.
To accomplish this, wrap the <paper-toggle-button>'s in an <iron-form>, and add name attributes to them. Then, insert an <input type="reset"> in the form, which serves as the reset button.
<iron-form>
<form>
<paper-toggle-button name="alice" checked="{{item.alice}}">Alice</paper-toggle-button>
<paper-toggle-button name="bob" checked="{{item.bob}}">Bob</paper-toggle-button>
<paper-toggle-button name="charlie" checked="{{item.charlie}}">Charlie</paper-toggle-button>
<paper-toggle-button name="dave" checked="{{item.dave}}">Dave</paper-toggle-button>
<input type="reset" class="save">
</form>
</iron-form>
demo

Angular 4 Create Dynamic formArray inside array using reactive forms

Here, we are creating dynamically form array's inside array.
Below is the sample structure of expected result given below.
{
"optionsRadios": null,
"Package_Title": null,
"HotelData": [
{
"Htitle": "",
"HDescription": "",
"hotelStar": "",
"RoomData": [
{
"Hotel_Room_Type": ""
},
{
"Hotel_Room_Type": ""
}
]
}
]
}
I want to create HotelData Dynamically, within that HotelData array i want to create RoomData array fields also dynamically.
I created HotelData fields by the following codes:
export class AddPackageComponent implements OnInit {
ngOnInit() {
this.invoiceForm = this._formBuild.group({
Package_Title: [],
HotelData: this._formBuild.array([this.addRows()])
})
}
addRows() {
return this._formBuild.group({
Htitle: [''],
HDescription: [''],
hotelStar: ['']
});
}
addHotel() {
const control: FormArray = this.invoiceForm.get(`HotelData`) as FormArray;
control.push(this.addRows());
}
}
You are on the right track, we just need to add some more code...
addRows need the form array RoomData, and here we also initially push an empty form group of room. If you don't want that, modify it.
addRows() {
let group = this._formBuild.group({
...
RoomData: this._formBuild.array([])
});
// push formgroup to array initially
this.addRoom(group.controls.RoomData)
return group;
}
addRoom looks like this:
addRoom(hotel:any) {
let group = this._formBuild.group({
Hotel_Room_Type: ['']
})
hotel.push(group)
}
addRoom is also the method we are calling from template when we want to add a new room to a hotel. Remember to pass the current hotel as parameter from template.
As for adding a new hotel, your addHotel stays the way you have it now.
Then over to your template, the relevant part should look something like this:
<div formArrayName="HotelData">
<div *ngFor="let hotel of invoiceForm.get('HotelData').controls; let i = index" [formGroupName]="i" >
<!-- form controls here -->
<button (click)="addRoom(hotel.get('RoomData'))">Add Room</button>
<div formArrayName="RoomData">
<div *ngFor="let room of hotel.get('RoomData').controls; let j = index" [formGroupName]="j">
<!-- form controls here -->
</div>
</div>
</div>
</div>
Finally, here's a Demo: http://plnkr.co/edit/7tcLcnzALew3oKGenjwK?p=preview

What is sane way in vuejs + vuex form handling?

I have a large forms to submit in single page.
<container>
<formA>
<formB>
<formC>
<submitButton>
<container>
it looks apparently like this. and I have a store which save every form data. then when user click submit button, I gather all form data using vuex store.
The problem is I need to update the form data in store everytime.
so I'll be like this in vue component
watch: {
userInput (val) {
this.updateState(val)
}
update state when input changes by watching form data(binded with v-model).
or like this which is documented in vuex doc.
userInput: {
get () {
return this.$store.state.userInput
},
set (val) {
this.updateState(val)
}
}
well.. I don't think these are good idea. Is there any better way to form handling with vuex?
I made a little tool which makes form handling wit Vuex a lot easier: vuex-map-fields
Example
Store
import Vue from 'vue';
import Vuex from 'vuex';
// Import the `getField` getter and the `updateField`
// mutation function from the `vuex-map-fields` module.
import { getField, updateField } from 'vuex-map-fields';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
fieldA: '',
fieldB: '',
},
getters: {
// Add the `getField` getter to the
// `getters` of your Vuex store instance.
getField,
},
mutations: {
// Add the `updateField` mutation to the
// `mutations` of your Vuex store instance.
updateField,
},
});
Component
<template>
<div id="app">
<input v-model="fieldA">
<input v-model="fieldB">
</div>
</template>
<script>
import { mapFields } from 'vuex-map-fields';
export default {
computed: {
// The `mapFields` function takes an array of
// field names and generates corresponding
// computed properties with getter and setter
// functions for accessing the Vuex store.
...mapFields([
'fieldA',
'fieldB',
]),
},
};
</script>
You can read more about vuex-map-fields on my blog: How to Handle Multi-row Forms with Vue, Vuex and vuex-map-fields
I would use deep watchers for this and have all fields in a object, you could use multiple approaches for saving the data, iterating over Object.keys to store each field with it's variable name in the form object, or storing the entire form, whatever you might need.
You could also use v-model.lazy="form.myfield" to indicate that you only want the binding to update once the user has left the field.
Form component
<template>
<div>
<!-- You can optionally use v-model.lazy="form.field1" to only update once user has exited the field or pressed enter -->
<input v-model="form.field1" />
<input v-model.lazy="form.field2" />
</div>
</template>
<script>
export default {
props: ['value'],
data: function () {
return {
internalForm: {
field1: null,
field2: null
}
}
},
watch: {
internalForm: {
handler: function (newValue) {
// Emit new form object to parent component so we can use v-model there
this.$emit('input', this.form)
// Or save form data
this.handleFormSave(this.form)
},
// Tell vue to do a deep watch of entire form object to watch child items in the object
deep: true
}
}
}
</script>
Parent component
<template>
<form-component v-model="forms.form1" />
<submit-button #click="saveAllFormData" />
</template>
<script>
export default {
data: function () {
return {
forms: {
form1: null // This will be updated when 'input' is emitted
}
}
},
watch: {
forms: {
handler: function (newValue) {
if (allFormsValid && readyToSave)
saveAllFormData(newValue);
},
deep: true
}
}
}
</script>
I had headache regarding this probem to.
Vuex doc describes that we need to update store for every field.
It's a loot of typing whatfor?
We make one solution that works.
It based on cloning store object to local one.
//We are passing (vuexstore) 'item' object from parent component:
//<common-item v-bind:item="item" ....
props: ['item'],
// create localItem - this is reactive object for vuex form
data: () => {
return {
localItem: null
}
},
// make clone on created event
created: function() {
this.localItem = this._clone(this.item)
},
// watch vuexstore 'item' for changes
watch: {
item: function(val) {
this.localItem = this._clone(this.item)
}
},
// map mutations and update store on event
methods: {
...mapMutations([
'editItem'
]),
updateItemHandler: function() {
this.editItem({ item: this._clone(this.localItem) })
},
_clone: function(o){
return JSON.parse(JSON.stringify(o))
}
},
Inside form use:
<input v-model="localItem.text" #keyup="updateItemHandler" type="text" class="form-control"></input>
I think this is only lack of vuex. There should be much shorter and built in solution.

How to change input value in redux

I am making a file manager app based on react-redux, and I meet problem with input.
For example, my code:
PathForm.js:
export default class PathForm extends Component {
render() {
const { currentPath, handleSubmit } = this.props;
console.log('PathFormPathFormPathForm', this.props)
return (
<div className="path-box">
<form onSubmit={handleSubmit}>
<div>
<input type="text" className="current-path-input" placeholder="input path" value={currentPath} />
</div>
<button className="go-btn" type="submit">Go</button>
</form>
</div>
);
}
}
Explorer.js:
class Explorer extends Component {
goPath(e) {
e.preventDefault()
// fake function here, because I have to solve the input problem first
console.log('PathForm goPath:',this.props)
let {targetPath , actions} = this.props
swal(targetPath)
}
render() {
const { node, currentPath , actions} = this.props
console.log('Explorer.render:',this.props)
return (
<div className='explorer-container'>
<PathForm currentPath={currentPath} handleSubmit={this.goPath.bind(this)}/>
<FileListOperator />
<FileListView fileList={node && node.childNodes} actions ={actions} />
</div>
);
}
}
function mapStateToProps(state, ownProps) {
return {
node: state.tree[state.tree.currentPath],
currentPath: state.tree.currentPath
};
}
function mapDispatchToProps(dispatch) {
console.log('mapDispatchToProps')
return {
actions: bindActionCreators(NodeActions, dispatch)
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Explorer);
Feature I want:
I have a PathForm, it need show path from two way:
user click a file path from left tree view, Explorer get this path as currentPath, then pass to PathForm, and show currentPath in input
user directly type a path to the PathForm's input, PathForm call handleSubmit(Explorer's function) to change the currentPath
Additional:I want to keep PathForm as a stateless component
The problem:
I'd like use PathForm as a stateless form, so I don't want connect it to store, but I need it change input by currentPath. But if I set value={currentPath}, user can not type anything else.
change to <input type="text" onChange={this.changeValue} value={this.getValue()}/> allow user type string in this input, but can not use props currentPath passed by Explorer
The only way I can imagine is connect this form to store which I don't want. I'd like Explorer to dispatch all actions and pass props.
Tried with some package
I found the input not act as my thought, so I tried the two popular package:
redux-form
It create a form need so much code, and official doc not say how to render this form with parent props,
I try to pass props and handleSubmit to it, not work. After I see
React + Redux - What's the best way to handle CRUD in a form component?
and How to wire up redux-form bindings to the form's inputs
I found I can't do that, it define some function overwrite mine, this behave is not good for me(I have to change the handlerSubmit function name, but it still not work), and it connect to the store. So I turn to formsy-react
formsy-react
It still need so much code, though it provide some mixin, but I still have to write a custom text input with changeValue function myself(changeValue is no need in most situation when writing normal html jquery app).Then I found the problem that PathForm can not use props currentPath passed by Explorer...
Probably Worked solution(but I don't tend to use):
connect PathForm to store, add another state inputPathValue for this input. Use inputPathValue interact with currentPath
After above, I found use input/form is super in-convenient in react....
Does it mean I have to connect PathForm to stroe?
Any other way to solve my problem?
There are uncontrolled(not set value) and controlled(set value) input in reactjs.
controlled not allow user input, but uncontrolled does.
Solution:
Need use uncontrolled input(no value attribute).
Select input element and set the value when currentPath change.
Bad way:
code:
export default class PathForm extends Component {
changeCurrentPath(path) {
const pathInput = document.querySelector('.current-path-input')
if (pathInput){
pathInput.value = path
this.lastPath = path
}
}
render() {
const { currentPath, handleSubmit } = this.props;
console.log('PathFormPathFormPathForm', this.props)
this.changeCurrentPath(currentPath)
return (
<div className="path-box">
<form onSubmit={handleSubmit}>
<div>
<input type="text" className="current-path-input" placeholder="input path" />
</div>
<button className="go-btn" type="submit">Go</button>
</form>
</div>
);
}
}
Good way:
use componentWillReceiveProps to set props and rel to select element
1.use form submit
export default class PathForm extends Component {
constructor(props) {
super(props)
// can not find `this` if not bind
this.handleSubmit = this.handleSubmit.bind(this)
}
componentWillReceiveProps(nextProps) {
if (nextProps.currentPath !== this.props.currentPath) {
this.setInputValue(nextProps.currentPath)
}
}
getInputValue() {
return this.refs.pathInput.value
}
setInputValue(val) {
this.refs.pathInput.value = val
}
handleSubmit(e){
e.preventDefault()
this.props.handleSubmit(this.getInputValue())
}
render() {
return (
<div className="path-box">
<form onSubmit={this.handleSubmit}>
<input className="current-path-input"
defaultValue={this.props.currentPath}
ref="pathInput" />
<button className="waves-effect waves-light btn" type="submit">Go</button>
</form>
</div>
);
}
}
2.use button click
export default class PathForm extends Component {
constructor(props) {
super(props)
// can not find `this` if not bind
this.handleGoClick = this.handleGoClick.bind(this)
this.handleKeyUp = this.handleKeyUp.bind(this)
}
componentWillReceiveProps(nextProps) {
if (nextProps.currentPath !== this.props.currentPath) {
this.setInputValue(nextProps.currentPath)
}
}
getInputValue() {
return this.refs.pathInput.value
}
setInputValue(val) {
this.refs.pathInput.value = val
}
handleKeyUp(e) {
if (e.keyCode === 13) {
this.handleGoClick()
}
}
handleGoClick(e) {
e.preventDefault()
this.props.handleSubmit(this.getInputValue())
}
render() {
return (
<div className="path-box">
<form >
<input className="current-path-input"
defaultValue={this.props.currentPath}
onKeyUp={this.handleKeyUp}
ref="pathInput" />
<button className="waves-effect waves-light btn" type="submit" onClick={this.handleGoClick}>Go</button>
</form>
</div>
);
}
}
If you really don't want the state in Redux, you can instead store the state on the component with setState. Directly accessing the input is strongly discouraged. You should track the state of the input on the component. Add an onChange handler to the input, store the state and handle componentWillReceiveProps where you decide what to do with new incoming props.