Marionette js 3+: whose responsibility to handle the situation when the validation failed? - backbone.js

There is view of registration form. When some one put data and click registration button: created model and after model.save() starting a validation.
The idea: when validation failed, mark form inputs as has-error
My code:
view
class App.Views.RegisterView extends Marionette.View
template: _.template(App.Templates['regFormTpl.ejs'])
ui:
registrate: '#reg-button'
events:
'click #ui.registrate': 'customerRegistration'
customerRegistration: (e) ->
e.preventDefault()
regData = new App.Models.RegData
login: $('#login').val()
email: $('#email').val()
password: $('#password').val()
confirmPassword: $('#password_confirm').val()
if regData.save()
console.log ('done')
else
console.log ('false')
model
class App.Models.RegData extends Backbone.Model
urlRoot: '/someurl'
defaults:
login: ''
email: ''
password: ''
confirmPassword: ''
validate: (attrs) ->
console.log(attrs)
errors = {}
if attrs.login.length < 5
errors.username = 'error desc'
if attrs.password != attrs.confirmPassword
errors.password = 'error desc'
unless _.isEmpty(errors)
return errors
I have my doubts about whose responsibility to handle the situation when the failed validation
I have no experience in the development of this framework. As I read the guides... earlier it was controller which trigger Event which one was listened by view.
In Mn 3+ controller was removed.

You can user your "View" itself as the responsible to handle the display of the errors in your form since this has access to the DOM elements.
In this example it is required the fields 'id' in your form and in your errors map matches.
customerRegistration: (e) ->
...
// remove previous errors
this.cleanFormErrors();
if (!regData.isValid()) {
_.each(regData.validationError, fieldName => this.showFormError(fieldName));
}
...
showFormError: (fieldId) ->
this.$el('[#' + fieldId + ']').addClass('has-error');
cleanFormErrors: () ->
this.$el.find('.has-error').removeClass('has-error');
Eventually these two methods can be delegated to a Marionette.Behavior to be able to reuse the logic in other Views, like:
class App.Behaviors.DisplayErrors extends Marionette.Behavior
onShowFormError: (fieldId) ->
this.$el('[#' + fieldId + ']').addClass('has-error');
onCleanFormErrors: () ->
this.$el.find('.has-error').removeClass('has-error');
Here is the modified View code using behaviors:
class App.Views.RegisterView extends Marionette.View
behaviors: {
'errorsBehavior': {
'behaviorClass': App.Behaviors.DisplayErrors
}
}
customerRegistration: (e) ->
...
this.triggerMethod('clean:form:errors');
if (!regData.isValid()) {
_.each(regData.validationError, fieldName => this.triggerMethod("show:form:errors"));
}
...
Hope this can guide you to solve your question.

Related

How to save draft of a long form in ReactJS

I have a long form with over 80 input fields. After submission, it works fine and saves data in a neo4j database. I need to have 'Save Draft' function in case the users want to fill the form later. How can I implement it ?
Edit:
I am trying to save the form data (including empty input fields) into the neo4j database.
I have an array and the form in the constructor:
constructor(props){
super(props);
this.state = {
mydata: {
data1: {},
data2: {},
}
};
this.saveAsDraft = this.saveAsDraft.bind(this);
<form>
<MyInput
title="Name"
inputs={[{title: "name", inputType: "text" }]}
onChange={this.handleChange}
type="freeText"
/>
<MyInput
title="Email"
inputs={[{title: "email", inputType: "text" }]}
onChange={this.handleChange}
type="freeText"
/>
</form>
}
And I used following method to handle the form submission:
async saveAsDraft(){
const name = this.state.mydata['data1']['name'];
const email = this.state.mydata['data1']['email'];
const neo4j = require('neo4j-driver')
const driver = neo4j.driver('bolt://localhost:7687', neo4j.auth.basic('neo4j', 'pass'))
const session = driver.session({database: 'neo4j'})
const txc = session.beginTransaction()
try {
const result = await txc.run(
'CREATE (:Person { name: $name, email: $email })',
{
name: name,
email: email
}
)
await txc.commit()
console.log('commited')
} catch (error) {
console.log(error)
await txc.rollback()
console.log('rolled back')
} finally {
await session.close()
}
}
It works when the input fields are filled. But it shows following error when input fields are empty:
Neo4jError: Expected parameter(s): name, email
how can i handle the empty input fields here ? I want to execute the cypher queries and create nodes and relations with empty input values.
I also need to extract the data later to populate them to the form when the user wants to continue editing the form later.
If it's required to access the form answers from different devices or browsers, then you'll have to store the data into database.
If it's enough, that the users can access the saved form data only on same browser, you can use local storage to achieve that. Some state management libraries like redux and mobxstate tree offer persist feature, so you can persist the whole state of your app in the local storage with minor config.

React form validation still adds values

So I have a little bit of form validation going on and I am running into an issue. When I first load the web app up and try adding a value and submitting with my button it doesn't allow me and gives me the error I want to see. However, when I add a value setState occurs and then my value is pushed to UI and I try to add another blank value it works and my conditional logic of checking for an empty string before doesn't not go through what am I doing wrong?
addItem() {
let todo = this.state.input;
let todos = this.state.todos;
let id = this.state.id;
if (this.state.input == '') {
alert("enter a value");
document.getElementById('error').style.color = 'red';
document.getElementById('error').innerHTML = 'Please enter something first';
}
else {
this.setState({
todos: todos.concat(todo),
id: id + 1,
}, () => {
document.getElementById('test').value = '';
})
console.log(this.state.id);
}
}
You are checking this.state.input but no where in that code are you setting the input value on the state.
Try adding this where it makes sense in your application:
this.setState({ input: 'some value' });
Also, I recommend you use the state to define the application UI. So instead of using document.getElementById('error') or document.getElementById('test').value, have the UI reflect what you have in your state.
See here for more info: https://reactjs.org/docs/forms.html
Instead of manipulating the DOM directly:
document.getElementById('test').value = '';
you'll want to use React:
this.setState({ input: '' });
A good ground rule for React is to not manipulate the DOM directly through calls like element.value = value or element.style.color = 'red'. This is what React (& setState) is for. Read more about this on reactjs.org.
Before you look for the solution of your issue, I noticed that you are directly updating the DOM
Examples
document.getElementById('error').style.color = 'red';
document.getElementById('error').innerHTML = 'Please enter something first';
document.getElementById('test').value = '';
Unless you have special use case or dealing with external plugins this isn't recommended, when dealing with React you should update using the virtual DOM. https://www.codecademy.com/articles/react-virtual-dom
Pseudo code sample
constructor(props) {
this.state = {
// retain previous states in here removed for example simplicity
errorString: ''
}
}
addItem() {
let todo = this.state.input;
let todos = this.state.todos;
let id = this.state.id;
if (this.state.input == '') {
alert("enter a value");
this.setState({
errorString: 'Please enter something first'
});
}
else {
this.setState({
todos: todos.concat(todo),
id: id + 1,
input: '',
});
}
}
// notice the "error" and "test" id this could be omitted I just added this for your reference since you mentioned those in your example.
render() {
return (
<div>
{(this.state.errorString !== '') ? <div id="error" style={{color: 'red'}}>{this.state.errorString}</div> : null}
<input id="test" value={this.state.input} />
</div>
}
Every time you invoke setState React will call render with the updated state this is the summary of what is happening but there are lot of things going behind setState including the involvement of Virtual DOM.

Angular, passing parameters in validation model driven

so here's my trouble, I'm validating Emails, and I'd like to be a bit permissive (depending on rather the email should be absolutely perfect or not), to do so I use a custom validator with a parameter withsuspicious (that'll trigger the exclusion of suspicious emails) so here's the code I have:
Component :
FormEmailAddress : FormGroup;
constructor(private fb: FormBuilder) { }
ngOnInit(){
this.FormEmailAddress = this.fb.group({
emailAddress: ['', Email.validateEmail]
});
}
Validator :
static validateEmail(control, withsuspicious) {
if (Validators.required(control)!== undefined && Validators.required(control)!== null) return null;
return Email.checkValidity(control.value, withsuspicious)? null : { pattern: {invalid: true}}
}
Email.checkValidity is just a function that validates email by returning true if good, false if not (with regexp and so on)
View
<form [formGroup]="FormEmailAddress" novalidate>
<h4>Email Model Driven</h4>
<input type="text" formControlName="emailAddress"/>
<p>Email : {{FormEmailAddress.get('emailAddress').value}}</p>
<p *ngIf="!FormEmailAddress.valid"> Wrong Email </p>
</form>
So I'd like to find a way to pass, as well as my "emailAddr" the parameter withsuspicious into my validator can you help me plz? Thanks
Finally found how to do it, didn't change anything in view or validator, juste did this on component :
this.FormEmailAddress = this.fb.group({
emailAddress: [''],
withsuspicious: [true]
}, {validator : this.validateEmail('emailAddress', 'withsuspicious')});
validateEmail(emailAddressKey, withsuspiciousKey){
return(group : FormGroup) => {
let emailInput = group.controls[emailAddressKey]
let withsuspiciousInput : boolean = group.controls[withsuspiciousKey].value
return Email.validateEmail(emailInput, withsuspiciousInput)
}
}

Binding Angular2 components inside of a Jquery plugin template

I'm working on using a kendo inside of an angular 2 project.
Getting the widget set up correctly is no problem:
ngOnInit() {
let options = inputsToOptionObject(KendoUIScheduler, this);
options.dataBound = this.bound;
this.scheduler = $(this.element.nativeElement)
.kendoScheduler(options)
.data('kendoScheduler');
}
When that runs, the plugin modifies the DOM (and, to my knowleged, without modifiying the shadow DOM maintained by angular2). My issue is that if I want to use a component anywhere inside of the plugin, like in a template, Angular is unaware of it's existence and won't bind it.
Example:
public views:kendo.ui.SchedulerView[] = [{
type: 'month',
title: 'test',
dayTemplate: (x:any) => {
let date = x.date.getDate();
let count = this.data[date];
return `<monthly-scheduler-day [date]="test" [count]=${count}"></monthly-scheduler-day>`
}
}];
The monthly-scheduler-day class:
#Component({
selector: 'monthly-scheduler-day',
template: `
<div>{{date}}</div>
<div class="badge" (click)=dayClick($event)>Available</div>
`
})
export class MonthlySchedulerDayComponent implements OnInit{
#Input() date: number;
#Input() count: number;
constructor() {
console.log('constructed');
}
ngOnInit(){
console.log('created');
}
dayClick(event){
console.log('clicked a day');
}
}
Is there a "right" way to bind these components inside of the markup created by the widget? I've managed to do it by listening for the bind event from the widget and then looping over the elements it created and using the DynamicComponentLoader, but it feels wrong.
I found some of the details I needed in this thread: https://github.com/angular/angular/issues/6223
I whipped this service up to handle binding my components:
import { Injectable, ComponentMetadata, ViewContainerRef, ComponentResolver, ComponentRef, Injector } from '#angular/core';
declare var $:JQueryStatic;
#Injectable()
export class JQueryBinder {
constructor(
private resolver: ComponentResolver,
private injector: Injector
){}
public bindAll(
componentType: any,
contextParser:(html:string)=>{},
componentInitializer:(c: ComponentRef<any>, context: {})=>void):
void
{
let selector = Reflect.getMetadata('annotations', componentType).find((a:any) => {
return a instanceof ComponentMetadata
}).selector;
this.resolver.resolveComponent(componentType).then((factory)=> {
$(selector).each((i,e) => {
let context = contextParser($(e).html());
let c = factory.create(this.injector, null, e);
componentInitializer(c, context);
c.changeDetectorRef.detectChanges();
c.onDestroy(()=>{
c.changeDetectorRef.detach();
})
});
});
}
}
Params:
componentType: The component class you want to bind. It uses reflection to pull the selector it needs
contextParser: callback that takes the existing child html and constructs a context object (anything you need to initialize the component state)
componentInitializer - callback that initializes the created component with the context you parsed
Example usage:
let parser = (html: string) => {
return {
date: parseInt(html)
};
};
let initer = (c: ComponentRef<GridCellComponent>, context: { date: number })=>{
let d = context.date;
c.instance.count = this.data[d];
c.instance.date = d;
}
this.binder.bindAll(GridCellComponent, parser, initer );
Well your solution works fine until the component needs to change its state and rerender some stuff.
Because I haven't found yet any ability to get ViewContainerRef for an element generated outside of Angular (jquery, vanilla js or even server-side)
the first idea was to call detectChanges() by setting up an interval. And after several iterations finally I came to a solution which works for me.
So far in 2017 you have to replace ComponentResolver with ComponentResolverFactory and do almost the same things:
let componentFactory = this.factoryResolver.resolveComponentFactory(componentType),
componentRef = componentFactory.create(this.injector, null, selectorOrNode);
componentRef.changeDetectorRef.detectChanges();
After that you can emulate attaching component instance to the change detection cycle by subscribing to EventEmitters of its NgZone:
let enumerateProperties = obj => Object.keys(obj).map(key => obj[key]),
properties = enumerateProperties(injector.get(NgZone))
.filter(p => p instanceof EventEmitter);
let subscriptions = Observable.merge(...properties)
.subscribe(_ => changeDetectorRef.detectChanges());
Of course don't forget to unsubscribe on destroy:
componentRef.onDestroy(_ => {
subscriptions.forEach(x => x.unsubscribe());
componentRef.changeDetectorRef.detach();
});
UPD after stackoverflowing once more
Forget all the words above. It works but just follow this answer

Binding event to function Coffeescript. Backbone.js

I am trying to fetch a JSON on OnChange event. And making changes to the existing haml page.
My aim is to populate another select field with the json values. At the moment I am just trying to print a message that json was fetched. But I get no change in the html.
In the network tab of the console, I see that the URL request is being made, and a 304 response is returned. I can also see the json in the response.
events:
"submit form" : "onSubmit"
"change [name=repository]" : "onChange"
onChange: (e) ->
e.preventDefault()
name = #$("[name=repository]").val()
localStorage['new_issue_last_repo'] = name
token = localStorage['oauth2_github']['accessToken']
#model = new Assignees(name)
console.log 'Printttttt'
#model.fetch {headers : #token},
success: (model) =>
#$('.assignee').html("<span>Fetched assignees</span>")
error: =>
#$('.assignee').html("<span>Failed to fetch assignees :(</span>")
The haml file looks like this.
.message
.assignee
%form
%section.repo-select
%select{name: 'repository'}
- for repository in #repositories.models
- full_name = repository.get('full_name')
- if localStorage['new_issue_last_repo'] == repository.get('full_name')
%option{value: full_name, selected: 'selected'}= repository.get('full_name')
- else
%option{value: full_name}= repository.get('full_name')
How can I get the .assignee to change once the json is fetched. Also how can I access the json data?
I have a similar function that works. I dont know what I am doing wrong in the onChange function.
onSubmit: (e) ->
e.preventDefault()
name = #$("[name=repository]").val()
localStorage['new_issue_last_repo'] = name
repository = #repositories.find (r) -> r.get('full_name') == name
model = new IssueModel({
body: #$("[name=body]").val()
title: #$("[name=title]").val()
assignee: #$("[name=assignee]").val()
milestone: #$("[name=milestone]").val()
}, {repository: repository})
model.save {},
success: (model) =>
#badge = new Badge()
#badge.addIssues(1)
#$('.message').html("<span>Issue ##{model.get('number')} was created!</span>")
error: =>
#$('.message').html("<span>Failed to create issue :(</span>")
I'm not that big on HAML but this:
%form
%section.repo-select
%select{name: 'repository'}
should become this HTML:
<form>
<section class="repo-select">
<select name="repository">
<!-- ... -->
</select>
</section>
</form>
right? That means that there is nothing that will match the ID-selector #repo-select so of course the handler bound to those events, onChange, will never be called.
If you want to get change-events from that <select>, then you'll want something like this in your events:
'change [name=repository]'
See Backbone's View#delegateEvents and jQuery's Selectors document for details.
As far as your messages go, I think you're a little confused about the difference between the Model#save arguments:
save model.save([attributes], [options])
and the Model#fetch arguments:
fetch model.save([options])
save takes two arguments with the callbacks in the second so this works:
model.save {},
success: (model) => ...
error: => ...
but fetch only takes one argument and the callbacks should be in that argument so this:
#model.fetch {headers : #token},
success: (model) => ...
error: => ...
won't work as fetch won't even see the success or error callbacks. You'd want to say this:
#model.fetch
headers: #token
success: (model) => ...
error: => ...
to get all three arguments to fetch.

Resources