How to prevent individual form elements from updating using Meteor + React? - mongodb

I have a Meteor + React single-page-application with a basic form in it. Data is collected from MongoDB using the createContainer method and passed to a form component. The problem I am facing is this. A user starts completing the form but, if the data that originally populated the form changes (by another user somewhere else in the world saving the form), the createContainer method will re-compute, which in turn pushes a new set of props to the form component and therefore overwrites what the user is typing in.
For many reasons, I cannot use the shouldComponentUpdate lifecycle method within the form component. One reason is that the form contains a select element, whose list of items should still accept reactive updates.
I need a way of halting certain reactive updates, but allowing others, whilst the user is completing the form. Any suggestions?
export default FormContainer = createContainer(( params ) => {
const dataFormHandle = Meteor.subscribe('FormsPub');
const dataFormIsReady = dataFormHandle.ready();
const dataListHandle = Meteor.subscribe('ListItemsPub');
const dataListIsReady = dataListHandle.ready();
let name = "";
let listItems = [];
let listSelectedValue = null;
if(dataListIsReady) {
listItems = collections.ListItemsColl.find({_id: ListId}).fetch();
}
if(dataFormIsReady) {
let formData = collections.FormsColl.find({_id: formId}).fetch();
name = formData[0].name;
listSelectedValue = formData[0].listSelectedValue;
}
return {
name,
listItems,
listSelectedValue
};
}, Form);
...
export default class Form extends Component {
constructor(props) {
super(props);
this.state = {
name: (this.props.name) ? this.props.name : "",
listSelectedValue: (this.props.listSelectedValue) ? this.props.listSelectedValue : null
};
}
componentWillReceiveProps(nextProps) {
this.setState({name: (nextProps.name) ? nextProps.name : ""});
this.setState({listSelectedValue: (nextProps.listSelectedValue) ? nextProps.listSelectedValue : null});
}
updateFormState(){
var name = e.target.name;
var val = e.target.value;
if(name == "name"){this.setState({name: val});}
if(name == "list"){
if( typeof e.target[e.target.selectedIndex] != "undefined" ) {
this.setState({listSelectedValue: val});
}
}
}
render(){
return (
<div>
<input type="text" name="name" value={this.state.name} onChange={this.updateFormState.bind(this)} />
<Select2
value={this.state.listSelectedValue}
name="list"
onChange={this.updateFormState.bind(this)}
options={{
minimumResultsForSearch: Infinity
}}
data={this.props.listItems}
/>
</div>
);
}
}

For the data in the form that you wish to be non-reactive simply specify reactive: false in your .find(), for example:
let formData = collections.FormsColl.find({ _id: formId },{ reactive: false }).fetch();
This will prevent the data from reactively updating while the form is open.

Related

Wagtail - how to get tags to work with `telepath` (tags in streamfield)?

I can use tags in regular page fields without any issue. When using tags within blocks (within a streamfield), the UI works and the tags are saved BUT the current page tags do not show up when loading the page in the admin. That's because the current value is not in the template anymore, it's in a JSON loaded via telepath.
I can confirm that the tags are saved and present in the data passed to initBlockWidget in the page source but these are ignored. Also, if I used a regular text field instead of the tag-widget, I can see the saved-values in the admin.
This is the code I have (which used to be enough before the refactor with telepath).
from wagtail.admin.widgets import AdminTagWidget
class TagBlock(TextBlock):
#cached_property
def field(self):
field_kwargs = {"widget": AdminTagWidget()}
field_kwargs.update(self.field_options)
return forms.CharField(**field_kwargs)
I think the following link is what I need to complete somehow to get it to work: https://docs.wagtail.io/en/stable/reference/streamfield/widget_api.html#form-widget-client-side-api
I've tried with this:
class AdminTagWidgetAdapter(WidgetAdapter):
class Media:
js = [
"wagtailadmin/js/vendor/tag-it.js",
"js/admin/admin-tag-widget-adapter.js",
]
register(AdminTagWidgetAdapter(), AdminTagWidget)
And under js/admin/admin-tag-widget-adapter.js:
console.log("adapter"); // this shows up in the console
class BoundWidget { // copied from wagtail source code
constructor(element, name, idForLabel, initialState) {
var selector = ':input[name="' + name + '"]';
this.input = element.find(selector).addBack(selector); // find, including element itself
this.idForLabel = idForLabel;
this.setState(initialState);
}
getValue() {
return this.input.val();
}
getState() {
return this.input.val();
}
setState(state) {
this.input.val(state);
}
getTextLabel(opts) {
const val = this.getValue();
if (typeof val !== 'string') return null;
const maxLength = opts && opts.maxLength;
if (maxLength && val.length > maxLength) {
return val.substring(0, maxLength - 1) + '…';
}
return val;
}
focus() {
this.input.focus();
}
}
// my code here:
class AdminTagWidget {
constructor(html, idPattern) {
this.html = html;
this.idPattern = idPattern;
}
boundWidgetClass = BoundWidget;
render(placeholder, name, id, initialState) {
console.log("RENDER", placeholder, name, id, initialState); // this does not show
var html = this.html.replace(/__NAME__/g, name).replace(/__ID__/g, id);
var idForLabel = this.idPattern.replace(/__ID__/g, id);
var dom = $(html);
$(placeholder).replaceWith(dom);
// eslint-disable-next-line new-cap
return new this.boundWidgetClass(dom, name, idForLabel, initialState);
}
}
console.log("here") // does show in the console
// variants I've tried:
//window.telepath.register('wagtail.admin.widgets.tags.AdminTagWidget', AdminTagWidget);
//window.telepath.register('wagtail.widgets.AdminTagWidget', AdminTagWidget);
window.telepath.register('path.where.its.used.AdminTagWidget', AdminTagWidget)
The log from my custom render method does not show. It seems that I'm not calling the right path within window.telepath.register but I don't know how what the string is supposed to be...
I'm not even sure if this is the right way forward.
Notes:
it works in regular field, the question is about tags in blocks
I'm using Wagtail version 2.13.2 but I've also tried with 2.15 without any difference.
In the console, I can log window.telepath and see my custom widget. It's just not "applied" to anything
Your WidgetAdapter class needs a js_constructor attribute:
class AdminTagWidgetAdapter(WidgetAdapter):
js_constructor = 'myapp.widgets.AdminTagWidget'
class Media:
js = [
"wagtailadmin/js/vendor/tag-it.js",
"js/admin/admin-tag-widget-adapter.js",
]
Any string value will work here - it just needs to uniquely identify the class, so it's recommended to use a dotted module-like path to avoid colliding with others. This then matches the string you pass to window.telepath.register on the Javascript side:
window.telepath.register('myapp.widgets.AdminTagWidget', AdminTagWidget)

Input type number "only numeric value" validation

How can I validate an input of type="number" to only be valid if the value is numeric or null using only Reactive Forms (no directives)?
Only numbers [0-9] and . are allowed, no "e" or any other characters.
What I've tried so far:
Template:
<form [formGroup]="form" novalidate>
<input type="number" formControlName="number" id="number">
</form>
Component:
export class App {
form: FormGroup = new FormGroup({});
constructor(
private fb: FormBuilder,
) {
this.form = fb.group({
number: ['', [CustomValidator.numeric]]
})
}
}
CustomValidator:
export class CustomValidator{
// Number only validation
static numeric(control: AbstractControl) {
let val = control.value;
if (val === null || val === '') return null;
if (!val.toString().match(/^[0-9]+(\.?[0-9]+)?$/)) return { 'invalidNumber': true };
return null;
}
}
Plunker
The problem is when a user enters something that is not a number ("123e" or "abc") the FormControl's value becomes null, keep in mind I don't want the field to be required so if the field really is empty null value should be valid.
Cross browser support is also important (Chrome's number input fields do not allow the user to input letters - except "e", but FireFox and Safari do).
In the HTML file, you can add ngIf for your pattern like this,
<div class="form-control-feedback" *ngIf="Mobile.errors && (Mobile.dirty || Mobile.touched)">
<p *ngIf="Mobile.errors.pattern" class="text-danger">Number Only</p>
</div>
In .ts file you can add the Validators pattern -"^[0-9]*$"
this.Mobile = new FormControl('', [
Validators.required,
Validators.pattern("^[0-9]*$"),
Validators.minLength(8),
]);
Using directive it becomes easy and can be used throughout the application
HTML
<input type="text" placeholder="Enter value" numbersOnly>
As .keyCode() and .which() are deprecated, codes are checked using .key()
Referred from
Directive:
#Directive({
selector: "[numbersOnly]"
})
export class NumbersOnlyDirective {
#Input() numbersOnly:boolean;
navigationKeys: Array<string> = ['Backspace']; //Add keys as per requirement
constructor(private _el: ElementRef) { }
#HostListener('keydown', ['$event']) onKeyDown(e: KeyboardEvent) {
if (
// Allow: Delete, Backspace, Tab, Escape, Enter, etc
this.navigationKeys.indexOf(e.key) > -1 ||
(e.key === 'a' && e.ctrlKey === true) || // Allow: Ctrl+A
(e.key === 'c' && e.ctrlKey === true) || // Allow: Ctrl+C
(e.key === 'v' && e.ctrlKey === true) || // Allow: Ctrl+V
(e.key === 'x' && e.ctrlKey === true) || // Allow: Ctrl+X
(e.key === 'a' && e.metaKey === true) || // Cmd+A (Mac)
(e.key === 'c' && e.metaKey === true) || // Cmd+C (Mac)
(e.key === 'v' && e.metaKey === true) || // Cmd+V (Mac)
(e.key === 'x' && e.metaKey === true) // Cmd+X (Mac)
) {
return; // let it happen, don't do anything
}
// Ensure that it is a number and stop the keypress
if (e.key === ' ' || isNaN(Number(e.key))) {
e.preventDefault();
}
}
}
Simplest and most effective way to do number validation is (it will restrict space and special character also)
if you dont want length restriction you can remove maxlength property
HTML
<input type="text" maxlength="3" (keypress)="validateNo($event)"/>
TS
validateNo(e): boolean {
const charCode = e.which ? e.which : e.keyCode;
if (charCode > 31 && (charCode < 48 || charCode > 57)) {
return false
}
return true
}
I had a similar problem, too: I wanted numbers and null on an input field that is not required. Worked through a number of different variations. I finally settled on this one, which seems to do the trick. You place a Directive, ntvFormValidity, on any form control that has native invalidity and that doesn't swizzle that invalid state into ng-invalid.
Sample use:
<input type="number" formControlName="num" placeholder="0" ntvFormValidity>
Directive definition:
import { Directive, Host, Self, ElementRef, AfterViewInit } from '#angular/core';
import { FormControlName, FormControl, Validators } from '#angular/forms';
#Directive({
selector: '[ntvFormValidity]'
})
export class NtvFormControlValidityDirective implements AfterViewInit {
constructor(#Host() private cn: FormControlName, #Host() private el: ElementRef) { }
/*
- Angular doesn't fire "change" events for invalid <input type="number">
- We have to check the DOM object for browser native invalid state
- Add custom validator that checks native invalidity
*/
ngAfterViewInit() {
var control: FormControl = this.cn.control;
// Bridge native invalid to ng-invalid via Validators
const ntvValidator = () => !this.el.nativeElement.validity.valid ? { error: "invalid" } : null;
const v_fn = control.validator;
control.setValidators(v_fn ? Validators.compose([v_fn, ntvValidator]) : ntvValidator);
setTimeout(()=>control.updateValueAndValidity(), 0);
}
}
The challenge was to get the ElementRef from the FormControl so that I could examine it. I know there's #ViewChild, but I didn't want to have to annotate each numeric input field with an ID and pass it to something else. So, I built a Directive which can ask for the ElementRef.
On Safari, for the HTML example above, Angular marks the form control invalid on inputs like "abc".
I think if I were to do this over, I'd probably build my own CVA for numeric input fields as that would provide even more control and make for a simple html.
Something like this:
<my-input-number formControlName="num" placeholder="0">
PS: If there's a better way to grab the FormControl for the directive, I'm guessing with Dependency Injection and providers on the declaration, please let me know so I can update my Directive (and this answer).
IMO the most robust and general way to do this is by checking if the value may be converted to number. For that add a validator:
numberValidator(control: FormControl) {
if (isNaN(control?.value)) {
return {
number: true
}
}
return null;
}
export class App {
form: FormGroup = new FormGroup({});
constructor(
private fb: FormBuilder,
) {
this.form = fb.group({
number: ['', [numberValidator]]
})
}
}
You can combine it with Validators.min and/or Validators.max to further limiting the accepted values.
The easiest way would be to use a library like this one and specifically you want noStrings to be true
export class CustomValidator{ // Number only validation
static numeric(control: AbstractControl) {
let val = control.value;
const hasError = validate({val: val}, {val: {numericality: {noStrings: true}}});
if (hasError) return null;
return val;
}
}
Try to put a minimum input and allow only numbers from 0 to 9. This worked for me in Angular Cli
<input type="number" oninput="this.value=this.value.replace(/[^\d]/,'')" min=0>
You need to use regular expressions in your custom validator. For example, here's the code that allows only 9 digits in the input fields:
function ssnValidator(control: FormControl): {[key: string]: any} {
const value: string = control.value || '';
const valid = value.match(/^\d{9}$/);
return valid ? null : {ssn: true};
}
Take a look at a sample app here:
https://github.com/Farata/angular2typescript/tree/master/Angular4/form-samples/src/app/reactive-validator
Sometimes it is just easier to try something simple like this.
validateNumber(control: FormControl): { [s: string]: boolean } {
//revised to reflect null as an acceptable value
if (control.value === null) return null;
// check to see if the control value is no a number
if (isNaN(control.value)) {
return { 'NaN': true };
}
return null;
}
Hope this helps.
updated as per comment,
You need to to call the validator like this
number: new FormControl('',[this.validateNumber.bind(this)])
The bind(this) is necessary if you are putting the validator in the component which is how I do it.

Setting URL in UploadCollectionItem in UI5 1.38.4

What parameters are mandatory for an UploadCollectionItem with the URL parameter set will show the file when the filename is clicked.
I am using a factory to handle files coming from different locations.
attachmentFactory(sId, context) {
const modelObj = context.getModel().getProperty(context.getPath());
const uploadListItem = new SAPUploadCollectionItem();
// If __metadata exists, attachment entry is from odata, if not then it's a FileEntry object.
if (modelObj.__metadata) {
uploadListItem.setFileName(modelObj.FILE_NAME);
uploadListItem.setMimeType(modelObj.MIME_CODE);
uploadListItem.setUrl("https://upload.wikimedia.org/wikipedia/commons/4/49/Koala_climbing_tree.jpg");
}
else {
uploadListItem.setFileName(modelObj.name);
uploadListItem.setMimeType(modelObj.type);
uploadListItem.setUrl("https://upload.wikimedia.org/wikipedia/commons/4/49/Koala_climbing_tree.jpg");
}
return uploadListItem;
}
I get an exception in UI5 when I press the link in the function
UploadCollection.prototype._triggerLink = function(oEvent, oContext) {
var iLine = null;
var aId;
if (oContext.editModeItem) {
//In case there is a list item in edit mode, the edit mode has to be finished first.
sap.m.UploadCollection.prototype._handleOk(oEvent, oContext, oContext.editModeItem, true);
if (oContext.sErrorState === "Error") {
//If there is an error, the link of the list item must not be triggered.
return this;
}
oContext.sFocusId = oEvent.getParameter("id");
}
aId = oEvent.oSource.getId().split("-");
iLine = aId[aId.length - 2];
sap.m.URLHelper.redirect(oContext.aItems[iLine].getProperty("url"), true);
};
oContext.aItems is an array but the source.getId() value is "__item9-ta_filenameHL" so __item9 is not found in oContext.aItems
I'm not sure if this is a bug or I'm setting up my UploadCollectionItem incorrectly
I had to set the sId of the UploadCollectionItem to be the sId that was passed into the factory.

Dynamically set form field values in React + Redux

My app's store has a store.authState subtree. In this subtree, there are three things, an authToken, an isFetching boolean and, most importantly a fields object. My form contains two fields : username and password.
I have created an action called SET_FORM_FIELD_VALUE which should generate and update each field's state as they are changed by the user.
I would like my SET_FORM_FIELD_VALUE to update the fields object. If a user normally fills in both username and password fields, my store.authState should look like this:
{
authToken: undefined,
isFetching: false,
fields: {
username: "Brachamul",
password: "azerty123"
}
}
However, it seems that my current code actually just overwrites everything and I end up with :
{
field: {
username: "Brachamul"
}
}
The data in the field changes based on what field I last edited. It's either username or password.
Here is my code :
switch (action.type) {
case 'SET_FORM_FIELD_VALUE':
let field = {} // create a "field" object that will represent the current field
field[action.fieldName] = action.fieldValue // give it the name and value of the current field
return { ...state.fields, field }
How can I change it to fix my issue ?
Your return is wrong, it should be something like this
switch (action.type) {
case 'SET_FORM_FIELD_VALUE':
return {
...state,
fields: {
...state.fields,
[action.fieldName] : action.fieldValue
}
}
}
Hope it helps.
i used change() from 'redux-form'
which only re rendered that specific form input, and isued it pretty often.
everytime the user clicked a dropdown menu it suggested values in 2 input fields
i abstracted away the html from the anwser and some other stuff.
import { FieldArray, Field, change, reduxForm } from 'redux-form';
class WizardFormThirdPage extends react.component{
runInject(target,value){
target.value= value; // sets the client html to the value
// change (formName, Fieldname, Value) in the state
this.props.dispatch(change('spray-record', target.name, value))
}
injectFormvalues(){
var tr = div.querySelector(".applicator-equipment");
var name = div.querySelector("select").value;
if(!name.includes("Select Employee")){
// var inputs = tr.querySelectorAll("input");
var employeeDoc= findApplicatorByName(name); // synchronous call to get info
tractor = tr.querySelector("input")
sprayer = tr.querySelectorAll("input")[1];
// here i send off the change attribute
this.runInject(tractor,Number(employeeDoc.default_t))
this.runInject(sprayer,Number(employeeDoc.default_s));
}
}
// you have to connect it get the dispatch event.
export default connect(state => ({
enableReinitialize: true,
}))(reduxForm({
form: "myFormName", // <------ same form name
destroyOnUnmount: false, // <------ preserve form dataLabel
forceUnregisterOnUnmount: true, // <------ unregister fields on unmount
validate,
})(WizardFormThirdPage));

Date formatting issues with Knockout and syncing to breeze.js entityAspect modified

Ok, so this is not a first, but I'm having a hard time getting a date. ;-)
I'm using Breeze, Knockout. Have a form where I wish to show short date.
<input name="start" data-bind="value: start" class="date required" required="required" placeholder="mm/dd/yyyy" style=" width:142px">
yields a long dateTime: Wed Aug 31 2011 20:00:00 GMT-0400 (Eastern Daylight Time).
Creating a method to format the desired short date accomplishes the goal of creating a short date, but my modelContext is unaware of any change notifications. So my object won't notify the screen of changes. I can possibly kludge this by trying to notify the dataContext on click, etc, but I'm hoping to not have that lost during the conversion.
function positionInitializer(posit) {
var shortDate = function (date) {
return date && moment.utc(date).isValid() ? moment.utc(date).format('L') : "";
};
posit.start = ko.observable(shortDate(posit.start()));
}
Are there any decent examples on how to do this?
I don't think I can convert when I make my call for the query b/c I am expanding the number of tables in my call & you can't do both.
var query = EntityQuery.from('Positions')
.where('id', '==', id)
.expand('Company, Projects')
.orderBy(orderBy.positions);
Thought I'd see what the hive-mind thinks...
There are a couple good options for handling date formatting using Knockout.
Writable Computed
You could create a writable computed for your date value and do all your formatting and parsing there. For example:
var myViewModel = function(){
var self=this;
self.trueDate = ko.observable(new Date());
self.formattedDate = ko.computed({
read: function(){
return moment(self.trueDate()).format('L');
},
write: function(value){
self.trueDate(moment(value).toDate());
}
});
}
<input type="text" data-bind="value: formattedDate" />
Any time the backing observable "trueDate" is updated its observers will be alerted.
Custom Binding
Another approach would be to build a custom data binding to format your data during binding and leave your view model simple.
var myViewModel = function(){
var self=this;
self.trueDate = ko.observable(new Date());
}
ko.bindingHandlers.dateString = {
init : function(element, valueAccessor) {
//attach an event handler to our dom element to handle user input
element.onchange = function(){
var value = valueAccessor();//get our observable
//set our observable to the parsed date from the input
value(moment(element.value).toDate());
};
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var value = valueAccessor();
var valueUnwrapped = ko.utils.unwrapObservable(value);
if (valueUnwrapped) {
element.value = moment(valueUnwrapped).format('L');
}
}
};
(Please keep in mind that the binding code above is untested, and doesn't check for invalid input, etc.)
And then your binding would be
<input type="text" data-bind="dateString : trueDate" />
I prefer the custom binding approach, since it can be easily reused for other dates and view models. A custom binding can also read other bindings on that element, so you could make the date format string configurable as a binding rather than hard-coding it to "L".
I hope this helps!
#RyanRahlf Your answer gave me some inspiration so offering up configurable formatting and date validation add-ons to your solution.
My situation was a tad different. My date is coming in as JSON string (Ex. 2013-08-02T00:00:00) so I needed two formats, one from JSON, the other to what will be displayed (user friendly)
ko.bindingHandlers.date = {
init: function (element, valueAccessor, allBindingsAccessor) {
var formats = allBindingsAccessor().dateFormats || { from: "", to: "" };
element.onchange = function () {
var observable = valueAccessor();
var value = moment(element.value)
if (value && value.isValid()) {
//if format is not set then assume observed is a js date
if (formats.from) {
observable(value.format(formats.from));
}
else {
observable(value.toDate());
}
}
else {
observable("");
//ensures element is blank when invalid input is attempted
if (element.value) element.value = "";
}
};
},
update: function (element, valueAccessor, allBindingsAccessor) {
var formats = allBindingsAccessor().dateFormats || { from: "", to: "MM/DD/YYYY" };
var observable = valueAccessor();
var valueUnwrapped = ko.utils.unwrapObservable(observable);
if (valueUnwrapped) {
element.value = moment(valueUnwrapped).format(formats.to);
}
else {
element.value = "";
}
}
};
use (dateFormats optional with defaults)
<input type="text" data-bind="date: trueDate, dateFormats: { from: 'YYYY-MM-DDTHH:mm:ss', to: 'YYYY/MM/DD' }" />
If you're already using jQuery datepicker and you don't want to add another javascript library you could use:
ko.bindingHandlers.textDate = {
update: function(element, valueAccessor, allBindingsAccessor, viewModel) {
var value = valueAccessor(),
allBindings = allBindingsAccessor(),
valueUnwrapped = ko.utils.unwrapObservable(value),
pattern = allBindings.datePattern || $.datepicker._defaults.dateFormat,
valueFormatted = $.datepicker.formatDate(pattern, valueUnwrapped);
$(element).text(valueFormatted);
}
};
However, it will only work with Date types.