Best practice to implement a form in javascript - javascript

I'm wondering about the most efficient, safe and smart way to code a complex form in javascript.
Often a user form can be very complex with a lot of different states, complex checks and so on, and to do a good job a good concept is absolutely necessary.
I really believe that the best solution is a state machine.
The following code is the form core logic I did for user registration with an RFID tag key, where user can register themselves using different credentials as phone, key RFID tag, mail, and complete their registration at a later stage with the same forum.
Consider just the logic behind it at high level.
The main loop which iterates on possible transitions (in order of priority):
/* Evaluate the actual form state (status) and check if it's possible to change
* to another status with a greater priority.
* If the state transition conditions are verified the transition callback is executed
* which, in case the state doesn't, is an empty function.
*/
setNextFormStatus: function(field) {
var sts,
that = this;
function conditionsAreValid(sts) {
var context = that.statusToConditionsMap[sts];
return ( sts == 'new_account' || that[context.field].state == context.state );
}
for (sts in this.statusToConditionsMap[this.status.actual].changes) {
var transition = this.statusToConditionsMap[this.status.actual].changes[sts];
if (transition && conditionsAreValid(sts)) {
if (sts != this.status.actual) {
this.status.previous = this.status.actual;
this.status.actual = sts;
this._resetForm(); // simple reset function
transition.apply(this);
}
break;
}
}
}
All status, their transition conditions, and their transition callbacks are defined in a dictionary when status are listed in order of priority:
/*
* This is the dictionary which defines form status states machine
*
* For each status are defined the following attributes:
* · state & field: define the condition to enter this status. The field must have that state (i.e. field.state == state)
* · changes (optional): the list of possible next status from this one, ordered by priority. Each status has an handle to call
*/
this.statusToConditionsMap = {
'registered_key':{
state: 'registered',
field: 'key'
},
'processed_key_with_username':{
state: 'unlinked',
field: 'key',
changes: {
'processed_key_with_username': function() {}, // empty cause callback is unnecessary
'new_account': this._checkNotEmptyFields
}
},
'phone_already_present_confirmed':{
state: 'already_present_confirmed',
field: 'phone',
changes: {
'phone_already_present_confirmed': function() {},
'phone_already_present_unconfirmed': this.phone_already_present_unconf_data_filler,
'new_account': this._checkNotEmptyFields
}
},
'phone_already_present_unconfirmed':{
state: 'already_present_unconfirmed',
field: 'phone',
changes: {
'phone_already_present_confirmed': this.phone_already_present_data_filler,
'phone_already_present_unconfirmed': function() {},
'new_account': this._checkNotEmptyFields
}
},
'email_already_present':{
state: 'email_already_present',
field: 'email',
changes: {
'phone_already_present_confirmed': this.phone_already_present_data_filler,
'phone_already_present_unconfirmed': this.phone_already_present_unconf_data_filler,
'email_already_present': function() {},
'new_account': this._checkNotEmptyFields
}
},
'new_account':{
state:'',
field: '',
changes: {
'registered_key': this.registered_key_data_filler,
'processed_key_with_username': this.processed_desikey_data_filler,
'phone_already_present_confirmed': this.phone_already_present_data_filler,
'phone_already_present_unconfirmed': this.phone_already_present_unconf_data_filler,
'email_already_present': function() {this.showMailCheckbox(); this.runCheck('phone');},
'new_account': function() {}
}
}
}
Which can be a best practice to implement a complex form?
Any other solution or method will be appreciated.

Related

How to pass/delete array params in HTTP Params with Angular

I have an Array of statuses objects. Every status has a name, and a boolean set at false by default.
It represent checkbox in a form with filters, when a checkbox is checked bool is set at true :
const filters.statuses = [
{
name: "pending",
value: false
},
{
name: "done",
value: false
},
];
I am using Angular HTTP Params to pass params at the URL.
filters.statuses.forEach((status) => {
if (status.value) {
this.urlParams = this.urlParams.append('statuses[]', status.name);
}
});
Url params looks like when a status is checked :
&statuses%5B%5D=pending
My problem is when I want to unchecked.
I know HTTP Params is Immutable, so, I'm trying to delete the param when checkbox is unchecked, so set to false :
...else {
this.urlParams = this.urlParams.delete('statuses');
}
But, it not works, URL doesn't change.
And if I re-check to true after that, the URL looks like :
&statuses%5B%5D=pending&statuses%5B%5D=pending
How can I delete params, if the status value is false, and keep others statuses in URL ?
Project on Angular 10.
Thanks for the help.
UPDATE : It works to delete, my param name was not good :
else {
this.urlParams = this.urlParams.delete('statuses[]', status.name);
}
But, my other problem, it's when I check 2 or more checkbox, the append function write on URL : &statuses%5B%5D=pending&statuses%5B%5D=pending&statuses%5B%5D=done
I have prepared an example to try to answer your question (If I understand this right way).
You can change the checkboxes state or the URL to play with it. Also, I added helper buttons, which will navigate you to different cases (by changing the URL).
Here is the example: https://stackblitz.com/edit/angular-router-basic-example-cffkwu?file=app/views/home/home.component.ts
There are some parts. We will talk about HomeComponent.
You have ngFor which displays statuses, I handled state using ngModel (you can choose whatever you want).
You have a subscription to the activatedRoute.queryParams observable, this is how you get params and set up checkboxes (the model of the checkboxes)
You have the ngModelChange handler, this is how you change the route according to the checkboxes state
Let's focus on 2 & 3 items.
The second one. Rendering the correct state according to the route query params. Here is the code:
ngOnInit() {
this.sub = this.activatedRoute.queryParams.subscribe((params) => {
const statusesFromParams = params?.statuses || [];
this.statuses = this.statuses.map((status) => {
if (statusesFromParams.includes(status.name)) {
return {
name: status.name,
active: true,
};
}
return {
name: status.name,
active: false,
};
});
});
}
Here I parse the statuses queryParam and I set up the statuses model. I decide which is active and which is not here.
The third one. You need to update the URL according to the checkboxes state. Here is the code:
// HTML
<ng-container *ngFor="let status of statuses">
{{ status.name}} <input type="checkbox" [(ngModel)]="status.active" (ngModelChange)="onInputChange()" /> <br />
</ng-container>
// TypeScript
onInputChange() {
this.router.navigate(['./'], {
relativeTo: this.activatedRoute,
queryParams: {
statuses: this.statuses
.filter((status) => status.active)
.map((status) => status.name),
},
});
}
Here you have the ngModelChange handler. When any checkbox is checked/unchecked this handler is invoked. In the handler, I use the navigate method of the Router to change the URL. I collect actual checkboxes state and build the query parameters for the navigation event.
So, now you have a binding of the checkboxes state to the URL and vice versa. Hope this helps.

Vue not updating input that depends on another

I'm building something like this, where the country I pick will dictate the phone country code automatically.
Both the country and countryCode are stored in a customer object, and when I'm changing the country's value, the trigger is correctly called and I can see the country code changing in Vue dev tools, however the related input does not update. This is my code:
data: function () {
return {
customer: {},
countries: this.$store.state.settings.countries,
}
},
created: function() {
var defaultCountry = _.find(this.countries, { default: true });
this.customer.country = defaultCountry.name;
this.customer.countryCode = defaultCountry.code;
},
methods: {
updateCountryCode: function(country) {
this.customer.countryCode = country.code;
},
}
And this is the relevant HTML:
<vSelect
label="name"
v-model="customer.country"
:options="countries"
:onChange="updateCountryCode">
</vSelect>
<input type="text" disabled :value="customer.countryCode">
What am I doing wrong? Why do I see the data being updated on dev tools but it doesn't act as reactive and my country code input stays the same?
You should define your customer object like so customer:
{
country: null, // or some other default value
countryCode: null,
},
You can find more details in the documentation here
This is how you shoul do it or #Nora's method of initializing the object properties to null
updateCountryCode: function(country) {
this.$set(this.customer, 'countryCode', country.code)
},
And the reason is because of change detection caveats in vue reactivity

PersistenceJS: Updating an existing database using migrate()

I'm currently trying to make changes to an existing DB using the migrations plugin for PersistenceJS. I can add/edit/delete items in the DB just fine — but…
How to add a column to an existing(!) table?
How to change the type of an existing(!) column, e.g. from 'text' to 'integer'?
These changes should retain currently existing data.
Sadly, the documentation is a little scarce, maybe you could help?
Here's the current, working setup:
persistence.store.websql.config(persistence, 'tododatabase', 'todos are fun', 5*1024*1024);
var Todo = persistence.define('Todo', {
task: 'TEXT',
priority: 'INT',
done: 'BOOL'
});
persistence.schemaSync();
function addTodo( item ){
var todo = new Todo();
todo.task = item.task;
todo.priority = item.priority;
todo.done = item.done;
persistence.add(todo);
persistence.flush();
};
function deleteTodo( item, callback ){
// item.id was created automatically by calling "new Todo()"
Todo.all().filter('id','=', item.id ).destroyAll( function(){
persistence.flush( callback );
});
};
The migration code that kinda works:
persistence.defineMigration(1, {
up: function() {
this.createTable('Todo', function(t){
t.text('task');
t.integer('priority');
t.boolean('done');
});
},
down: function() {
this.dropTable('Todo');
}
});
persistence.defineMigration(2, {
up: function() {
this.addColumn('Todo', 'due', 'DATE');
},
down: function() {
this.removeColumn('Todo', 'due');
}
});
function migrate( callback ){
console.log('migrating...');
persistence.migrations.init( function(){
console.log('migration init');
// this should migrate up to the latest version, in our case: 2
persistence.migrate( function(){
console.log('migration complete!');
} );
});
}
Results…
calling migrate() will only log up to "migration init", the complete handler is never called, the "due" column is not created
not calling schemaSync() before calling migrate() as Zef Hemel himself proposed in this post yields the same result as 1.
changing the first line to persistence.store.websql.config(persistence, 'newdatabase', 'testing migration', 5*1024*1024);, not calling schemaSync() and only calling migrate() will successfully log "migration complete!" — but it does so in a new, completely empty database "newdatabase", which will of course not retain any exsiting data.
Summary
There is a database that was created using persistence.store.websql.config(...), persistence.define('Todo',...) and persistence.schemaSync().
I now want to keep all the data that already exist in that database, but want to
change the type of column priority from 'integer' to 'text'
add a column due with type 'date' to all existing Todos
If you could push me in the right direction, I'd greatly appreciate it!
Thanks!
I finally got it working. There are a number of issues with my initial requirements that I'd like to point out for future reference. Take a look at the first migration definition:
persistence.defineMigration(1, {
up: function() {
this.createTable('Todo', function(t){
...
Not surprisingly, createTable will do exactly that: it will execute the SQL statement 'CREATE TABLE Todo ...', which will silently fail and halt the migration if there is a table with the name Todo already. This is why it worked with a new database, but not with the existing one. Bear in mind: I already had a live database with a table "Todo" that needed updating. If you're starting fresh (i.e. you've not used schemaSync), createTable works just fine. Since the Migrations plugin does not provide a createTableIfNotExists method, I needed to utilize executeSql as follows:
persistence.defineMigration(1, {
up: function() {
this.executeSql('CREATE TABLE IF NOT EXISTS Todo (id VARCHAR(32) PRIMARY KEY, task TEXT, priority INT, done BOOL)');
...
Now that the migration from schema version 0 to 1 succeeded, the migration to version 2 was successful as well.
With the migration to version 3 the type of the priority column needed to change from int to text. This would normally be done using the ALTER COLUMN SQL command, wich is not supported by Web SQL / SQLite. See Omitted Features for SQLite.
Altering a column with SQLite requires a 4-step workaround:
persistence.defineMigration(3, {
up: function() {
// rename current table
this.executeSql('ALTER TABLE Todo RENAME TO OldTodo');
// create new table with required columns and column types
this.executeSql('CREATE TABLE Todo (id VARCHAR(32) PRIMARY KEY, task TEXT, priority TEXT, done BOOL)');
// copy contents from old table to new table
this.executeSql('INSERT INTO Todo(id, task, priority, done) SELECT id, task, priority, done FROM OldTodo');
// delete old table
this.executeSql('DROP TABLE OldTodo');
},
...
Of course, after changing the column type, the entity definition for 'Todo' should also be changed:
var Todo = persistence.define('Todo', {
task: 'TEXT',
priority: 'TEXT', // was 'INT'
due: 'DATE',
done: 'BOOL'
});
And finally, the complete source:
persistence.store.websql.config(persistence, 'tododatabase', 'todos are fun', 5*1024*1024);
// persistence.debug = true;
//v0 + v1
// var Todo = persistence.define('Todo', {
// task: 'TEXT',
// priority: 'INT',
// done: 'BOOL'
// });
//v2
// var Todo = persistence.define('Todo', {
// task: 'TEXT',
// priority: 'INT',
// due: 'DATE',
// done: 'BOOL'
// });
//v3
var Todo = persistence.define('Todo', {
task: 'TEXT',
priority: 'TEXT',
due: 'DATE',
done: 'BOOL'
});
persistence.defineMigration(1, {
up: function() {
this.executeSql('CREATE TABLE IF NOT EXISTS Todo (id VARCHAR(32) PRIMARY KEY, task TEXT, priority INT, done BOOL)');
},
down: function() {
this.dropTable('Todo');
}
});
persistence.defineMigration(2, {
up: function() {
this.addColumn('Todo', 'due', 'DATE');
},
down: function() {
this.removeColumn('Todo', 'due');
}
});
persistence.defineMigration(3, {
up: function() {
// rename current table
this.executeSql('ALTER TABLE Todo RENAME TO OldTodo');
// create new table with required columns
this.executeSql('CREATE TABLE Todo (id VARCHAR(32) PRIMARY KEY, task TEXT, priority TEXT, due DATE, done BOOL)');
// copy contents from old table to new table
this.executeSql('INSERT INTO Todo(id, task, priority, due, done) SELECT id, task, priority, due, done FROM OldTodo');
// delete current table
this.executeSql('DROP TABLE OldTodo');
},
down: function() {
this.executeSql('ALTER TABLE Todo RENAME TO OldTodo');
this.executeSql('CREATE TABLE Todo (id VARCHAR(32) PRIMARY KEY, task TEXT, priority INT, due DATE, done BOOL)');
this.executeSql('INSERT INTO Todo(id, task, priority, due, done) SELECT id, task, priority, due, done FROM OldTodo');
this.executeSql('DROP TABLE OldTodo');
}
});
function migrate( callback ){
console.log('migrating...');
persistence.migrations.init( function(){
console.log('migration init');
persistence.migrate( function(){
console.debug('migration complete!');
callback();
} );
});
};
migrate( onMigrationComplete );
function onMigrationComplete(){
// database is ready. do amazing things...
};
That's a great explanation, thank you! But I think I know an easier way to achieve this.
I got in the same trouble like you: I'v got a set of schemas described with persistence.define and created with persistence.schemaSync.
So this is my particular case:
// This is my mixin for all schemas
var Versioned = persistence.defineMixin('Versioned', {
serverId: "TEXT",
intVersion: "INT",
dtSynced: "DATE",
dtCreatedAt: "DATE",
dtUpdatedAt: "DATE",
delete: "BOOL",
update: "BOOL",
add: "BOOL",
isReadOnly: "BOOL"
});
// This is one of the schemas I need to update with a new field.
var Person = persistence.define('Person', {
fullName: "TEXT",
rate: "INT"
});
//... More schema definitions
// Setup mixin
Person.is(Versioned);
// Sync schemas
persistence.schemaSync();
Ok. Nothing special about it. Now after a few months my app's being in production I want to add a new field isEmployed to the Person schema.
According to the docs I should rewrite all of my schema definitions to the migrations and to stop using persistence.schemaSync(). But I don't want to rewrite all of my definitions. Instead of it I define a new migration right behind the PersistenceJS init code:
// Init ORM
persistence.store.websql.config(
persistence,
'Sarafan',
'0.0.2',
'Sarafan.app database',
100 * 1024 * 1024,
0
);
// Define Migrations
persistence.defineMigration(1, {
up: function () {
this.addColumn('Person', 'isEmployed', 'BOOL');
}
});
// ... describing isVersioned mixin
// Updated schema definition with a new field 'isEmployed'
var Person = persistence.define('Person', {
fullName: "TEXT",
rate: "INT",
isEmployed: "BOOL"
});
//... More schema definitions
// Setup mixin
Person.is(Versioned);
// Apply the migration right away from the schemaSync call.
persistence.schemaSync(function (tx) {
persistence.migrations.init(function () {
persistence.migrate(function(){
// Optional callback to be executed after initialization
});
});
});
So that's it! I tested this approach only to add new fields to the schema.
Let me know if it does or doesn't work for you.

How to get a Custom ExtJS Component to render some html based on a bound value

I'm trying to get a custom extjs component to render either a green-check or red-x image, based on a true/false value being bound to it.
There's a couple of other controls that previous developers have written for rendering custom labels/custom buttons that I'm trying to base my control off but I'm not having much luck.
I'd like to be able to use it in a view as follows where "recordIsValid" is the name of the property in my model. (If I remove the xtype: it just renders as true/false)
{
"xtype": "booldisplayfield",
"name": "recordIsValid"
}
Here's what I have so far, but ExtJS is pretty foreign to me.
Ext.define('MyApp.view.ux.form.BoolDisplayField', {
extend: 'Ext.Component',
alias : 'widget.booldisplayfield',
renderTpl : '<img src="{value}" />',
autoEl: 'img',
config: {
value: ''
},
initComponent: function () {
var me = this;
me.callParent(arguments);
this.renderData = {
value: this.getValue()
};
},
getValue: function () {
return this.value;
},
setValue: function (v) {
if(v){
this.value = "/Images/booltrue.png";
}else{
this.value = "/Images/boolfalse.png";
}
return this;
}
});
I'd taken most of the above from a previous custom linkbutton implementation. I was assuming that setValue would be called when the model-value for recordIsValid is bound to the control. Then based on whether that was true or false, it would override setting the value property of the control with the correct image.
And then in the initComponent, it would set the renderData value by calling getValue and that this would be injected into the renderTpl string.
Any help would be greatly appreciated.
You should use the tpl option instead of the renderTpl one. The later is intended for rendering the component structure, rather that its content. This way, you'll be able to use the update method to update the component.
You also need to call initConfig in your component's constructor for the initial state to be applied.
Finally, I advice to use applyValue instead of setValue for semantical reasons, and to keep the boolean value for getValue/setValue.
Ext.define('MyApp.view.ux.form.BoolDisplayField', {
extend: 'Ext.Component',
alias : 'widget.booldisplayfield',
tpl: '<img src="{src}" />',
config: {
// I think you should keep the true value in there
// (in order for setValue/getValue to yield the expected
// result)
value: false
},
constructor: function(config) {
// will trigger applyValue
this.initConfig(config);
this.callParent(arguments);
},
// You can do this in setValue, but since you're using
// a config option (for value), it is semantically more
// appropriate to use applyValue. setValue & getValue
// will be generated anyway.
applyValue: function(v) {
if (v) {
this.update({
src: "/Images/booltrue.png"
});
}else{
this.update({
src: "/Images/boolfalse.png"
});
}
return v;
}
});
With that, you can set your value either at creation time, or later, using setValue.
// Initial value
var c = Ext.create('MyApp.view.ux.form.BoolDisplayField', {
renderTo: Ext.getBody()
,value: false
});
// ... that you can change later
c.setValue(true);
However, you won't be able to drop this component as it is in an Ext form and have it acting as a full fledged field. That is, its value won't be set, retrieved, etc. For that, you'll have to use the Ext.form.field.Field mixin. See this other question for an extended discussion on the subject.

Enyo custom properties

I try to create my own kind in enyo
enyo.kind(
{
name: "DeviceButton",
kind: "Button",
caption: "",
published: { userAgent: "" },
flex: 1,
onclick: "butclick",
butclick: function() { console.log("User agent changed to " + this.userAgent) }
})
But when I click there is nothing shown
If I just did
onclick: console.log("User agent changed to " + this.userAgent)
It was printed that this.userAgent is undefined
What am I doing wrong?
Btw., is it possible to send parameters via onclick (so that the function repsponding to the click get a variable)
Thanks
The problem you're having here is that the onclick property is actually giving the name of the event handler for the Enyo to send the event to when the click is received. The "butclick" event isn't dispatched to the DeviceButton, but rather to its parent.
If you want to handle the event entirely within your kind, then you need to set it up as a "handler". In Enyo 2.x, you do it like this:
enyo.kind(
{
name: "DeviceButton",
kind: "Button",
caption: "",
published: { userAgent: "" },
flex: 1,
handlers {
onclick: "butclick"
},
butclick: function() { console.log("User agent changed to " + this.userAgent) }
})
In Enyo 1.x, you'd just need to name the handler function "onclickHandler". I mention the Enyo 1 solution because I see that you have "flex: 1" in your definition. Flexbox isn't supported in Enyo 2, we have a "Fittable" system instead.
I made a little example for you how enyo can handle sending and receiving values to and from a custom kind. I also added some short comments inside the code.
http://jsfiddle.net/joopmicroop/K3azX/
enyo.kind({
name: 'App',
kind: enyo.Control,
components: [
{kind:'FittableRows', components:[
// calls the custom kind by it's default values
{kind:'DeviceButton',name:'bttn1', classes:'joop-btn',ontap:'printToTarget'},
// calls the custom kind and pass the variables to the DeviceButton kind
{kind:'DeviceButton', name:'bttn2', btnstring:'Get Agent', useragent:'chrome', classes:'joop-btn', ontap:'printToTarget'},
{tag:'div', name:'targetContainer', content:'no button clicked yet', classes:'joop-target'},
]},
],
printToTarget:function(inSender, inEvent){
// inSender = the button that was pressed
this.$.targetContainer.setContent(inSender.name+' has used the value: "'+inSender.getUseragent()+'" and sends the value of: "'+inSender.getValueToPrint()+'" back.');
},
});
enyo.kind({
name:'DeviceButton',
kind:enyo.Control,
components:[
{kind:'onyx.Button',name:'btn', ontap:'printUsrAgent'}
],
published:{
btnstring:'default btnstring', // value that will be received
useragent:'default useragent', // value that will be received
valueToPrint:'default valueToPrint' // value that will be used
},
rendered:function(){
this.inherited(arguments);
this.$.btn.setContent(this.btnstring);
},
printUsrAgent:function(inSender,inEvent){
// set a parameter with the value that was received of if not received use the default value (normaly would do some calculations with it first)
this.valueToPrint = this.useragent+' after changes';
},
});
​

Categories

Resources