React pass all events to child component - javascript

Is it possible in React to pass all event to child element.
as an example I've got a custom Button class, that (simplified) looks something like this:
class Button extends Component {
constructor (props) {
super(props);
this.onClick = this.onClick.bind(this);
}
/* .... */
onClick (ev) {
const { disabled, onClick } = this.props;
if (!disabled) {
onClick(ev);
}
}
render () {
const {
children,
disabled,
type
} = this.props;
return (
<button
disabled={disabled}
onClick={this.onClick}
ref="button"
type={type}
>{children}</button>
}
}
I don't know what events i may want to use in the future (onMouseDown, onMouseUp, onBlur, onKeyDown, onTouchStart, and so on...)
Is it possible to pass all possible events to the button element without writing out a prop for every possible event?
adding {...this.props} to the button element is not what I want because it passes all props and some props (like className which is omitted in this example) should not be passed directly.
I thought about cloning the props object and deleting the props which should not be passed directly but this feels like a hack. Does anybody know a cleaner way?

I've written a function to iterate over the props and filter out all properties starting with 'on' this is the closest I've come so far. In case it helps anyone else:
/* helpers.js */
export function filterEvents (props, ignore = []) {
let events = {};
for (let property in props) {
if (props.hasOwnProperty(property)) {
if (property.startsWith('on') && ignore.indexOf(property) === -1) {
events[property] = props[property];
}
}
}
return events;
}
/* Tests for the filterEvents */
import { expect } from 'chai';
import { filterEvents } from './helpers';
describe('filterEvents', () => {
const props = {
className: 'someClass',
disabled: true,
onBlur: 'onBlur',
onClick: 'onClick',
onMouseDown: 'onMouseDown',
onMouseUp: 'onMouseUp'
};
it('only returns keys starting with on', () => {
const expected = {
onBlur: 'onBlur',
onClick: 'onClick',
onMouseDown: 'onMouseDown',
onMouseUp: 'onMouseUp'
};
expect(filterEvents(props)).to.deep.equal(expected);
});
it('only returns keys starting with on minus the ones in the ignore array', () => {
const expected = {
onBlur: 'onBlur',
onMouseUp: 'onMouseUp'
};
const ignore = ['onClick', 'onMouseDown'];
expect(filterEvents(props, ignore)).to.deep.equal(expected);
});
});
/* Using the function inside a component */
import { filterEvents } from './helpers'; //at the top of the components file
//Inside the render method:
const events = filterEvents(this.props, ['onClick']); //don't include onClick it's handled like the questions example
return (
<button
disabled={this.props.disabled}
onClick={this.onClick}
{...events}
>
{this.props.children}
</button>
);

I've taken Barry127's answer and added all the event handlers from React and put them in an object.
const acceptedEventHandlersForComponentValidationFunction = {
clipBoard: [
"onCopy",
"onCut",
"onPaste",
"onCopyCapture",
"onCutCapture",
"onPasteCapture"
],
composition: [
"onCompositionEnd",
"onCompositionStart",
"onCompositionUpdate",
"onCompositionEndCapture",
"onCompositionStartCapture",
"onCompositionUpdateCapture"
],
keyboard: [
"onKeyDown",
"onKeyPress",
"onKeyUp",
"onKeyDownCapture",
"onKeyPressCapture",
"onKeyUpCapture"
],
focus: ["onFocus", "onBlur", "onFocusCapture", "onBlurCapture"],
form: [
"onChange",
"onInput",
"onInvalid",
"onReset",
"onSubmit",
"onChangeCapture",
"onInputCapture",
"onInvalidCapture",
"onResetCapture",
"onSubmitCapture"
],
generic: ["onError", "onLoad", "onErrorCapture", "onLoadCapture"],
mouse: [
"onClick",
"onContextMenu",
"onDoubleClick",
"onDrag",
"onDragEnd",
"onDragEnter",
"onDragExit",
"onDragLeave",
"onDragOver",
"onDragStart",
"onDrop",
"onMouseDown",
"onMouseEnter",
"onMouseLeave",
"onMouseMove",
"onMouseOut",
"onMouseOver",
"onMouseUp",
"onClickCapture",
"onContextMenuCapture",
"onDoubleClickCapture",
"onDragCapture",
"onDragEndCapture",
"onDragEnterCapture",
"onDragExitCapture",
"onDragLeaveCapture",
"onDragOverCapture",
"onDragStartCapture",
"onDropCapture",
"onMouseDownCapture",
"onMouseMoveCapture",
"onMouseOutCapture",
"onMouseOverCapture",
"onMouseUpCapture"
],
pointer: [
"onPointerDown",
"onPointerMove",
"onPointerUp",
"onPointerCancel",
"onGotPointerCapture",
"onLostPointerCapture",
"onPointerEnter",
"onPointerLeave",
"onPointerOver",
"onPointerOut",
"onPointerDownCapture",
"onPointerMoveCapture",
"onPointerUpCapture",
"onPointerCancelCapture",
"onGotPointerCaptureCapture",
"onLostPointerCaptureCapture",
"onPointerOverCapture",
"onPointerOutCapture"
],
selection: ["onSelect", "onSelectCapture"],
touch: [
"onTouchCancel",
"onTouchEnd",
"onTouchMove",
"onTouchStart",
"onTouchCancelCapture",
"onTouchEndCapture",
"onTouchMoveCapture",
"onTouchStartCapture"
],
ui: ["onScroll", "onScrollCapture"],
wheel: ["onWheel", "onWheelCapture"],
media: [
"onAbort",
"onCanPlay",
"onCanPlayThrough",
"onDurationChange",
"onEmptied",
"onEncrypted",
"onEnded",
"onError",
"onLoadedData",
"onLoadedMetadata",
"onLoadStart",
"onPause",
"onPlay",
"onPlaying",
"onProgress",
"onRateChange",
"onSeeked",
"onSeeking",
"onStalled",
"onSuspend",
"onTimeUpdate",
"onVolumeChange",
"onWaiting",
"onAbortCapture",
"onCanPlayCapture",
"onCanPlayThroughCapture",
"onDurationChangeCapture",
"onEmptiedCapture",
"onEncryptedCapture",
"onEndedCapture",
"onErrorCapture",
"onLoadedDataCapture",
"onLoadedMetadataCapture",
"onLoadStartCapture",
"onPauseCapture",
"onPlayCapture",
"onPlayingCapture",
"onProgressCapture",
"onRateChangeCapture",
"onSeekedCapture",
"onSeekingCapture",
"onStalledCapture",
"onSuspendCapture",
"onTimeUpdateCapture",
"onVolumeChangeCapture",
"onWaitingCapture"
],
image: ["onLoad", "onError", "onLoadCapture", "onErrorCapture"],
animation: [
"onAnimationStart",
"onAnimationEnd",
"onAnimationIteration",
"onAnimationStartCapture",
"onAnimationEndCapture",
"onAnimationIterationCapture"
],
transition: ["onTransitionEnd", "onTransitionEndCapture"],
other: ["onToggle", "onToggleCapture"]
}
/*
- Component props event handler vilidation
Return all valid events to be used on a component
{
acceptedEventHandlerTypes: [
"${event handler type}"
],
eventHandlers: {
${event}: "${callback function}" // ${event} can contain "Capture" at the end to register the event handler for the capture phase
}
}
*/
const validateComponentPropsEventHandlers = (
acceptedEventHandlerTypes,
eventHandlers = {}
) => {
if (Object.keys(eventHandlers).length == 0) {
return {}
}
// Fill eventsForSpecifiedType with only the required events
let eventsForSpecifiedType = {}
let eventsCount = 0
for (const eventHandlerType in acceptedEventHandlerTypes) {
if (
acceptedEventHandlerTypes[eventHandlerType] in
acceptedEventHandlersForComponentValidationFunction
) {
const newEvents =
acceptedEventHandlersForComponentValidationFunction[
acceptedEventHandlerTypes[eventHandlerType]
]
eventsForSpecifiedType[
acceptedEventHandlerTypes[eventHandlerType]
] = newEvents
eventsCount += newEvents.length
}
}
// Fill events
let events = {}
let eventsCountCheck = 0
const checkIfEventsCountHasBeenReached = () =>
eventsCountCheck == eventsCount
for (const eventHandler in eventHandlers) {
if (checkIfEventsCountHasBeenReached()) {
return events
}
// Append event handler to events object if it is recognised
Object.values(eventsForSpecifiedType).forEach(
(EVENT_HANDLERS) => {
if (
EVENT_HANDLERS.includes(eventHandler) &&
!(eventHandler in events)
) {
events[eventHandler] = eventHandlers[eventHandler]
eventsCountCheck += 1
}
if (checkIfEventsCountHasBeenReached()) {
return events
}
}
)
}
return events
}
// Usage
const test = () => {console.log("test")}
const events = validateComponentPropsEventHandlers(["mouse"], { onClick: test })
console.log(events)
// <button {...events}>Button</button>

Related

Vue3 Web Components events not working with custom wrapper

The WebComponents do work fine with this custom script but I have problems to.
I want to be able to add event listener from outside to listen to the custom Vue events fired from within the component (in this example on click on the WC). Thus I created an event proxy but for some reason the listener is never triggered. Obviously I made a mistake somewhere. So I hope that someone can point me to the right solution.
I made a CodeSandbox for you to take a look and play around with the code.
(as CodeSandbox has some issues you'll need to click on the reload button on the right side of the layout in order to make it work)
//main.js
import { createApp } from "vue";
import App from "./App.vue";
import WebComponentService from "./services/wc";
WebComponentService.init();
createApp(App).mount("#app");
Vue Component used as source file for Web Component
//src/wcs/MyComp.vue
<template>
<div class="m" #click="click">
Hello My Comp {{ name }}
<div v-for="(i, index) in values" :key="index">ID: {{ i.title }}</div>
</div>
</template>
<script>
export default {
__useShadowDom: false,
emits: ["my-click"],
// emits: ["my-click", "myclick"],
props: {
name: {
default: "Test",
},
values: {
type: Object,
default: () => {
return [{ title: "A" }, { title: "B" }];
},
},
},
methods: {
click() {
// emit the event
this.$emit("my-click");
console.log("EMIT");
},
},
};
</script>
<style lang="less" scoped>
.m {
border: 5px solid red;
margin: 10px;
padding: 10px;
border-radius: 5px;
}
</style>
Index.html where I test the web component and added an event listener
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
<h3>WC Test here</h3>
<div class="component-canvas">
<!-- web component testing -->
<xvue-my-comp
id="vtest"
name="MyComp HTML Rendering"
:values='[{"title": "Element 1"}, {"title": "Element"}]'
>Inner HTML Caption</xvue-my-comp
>
</div>
<script>
const mySelector = document.querySelector("xvue-my-comp");
//listen to the event
mySelector.addEventListener("my-click", function (ev) {
console.log("#LISTEN");
alert(ev);
});
</script>
</body>
</html>
Custom WebComponents wrapper
import HTMLParsedElement from "html-parsed-element";
import { createApp, h, toHandlerKey } from "vue";
import { snakeCase, camelCase } from "lodash";
let registeredComponents = {};
const tagPrefix = "xvue-"; // Prefix for Custom Elements (HTML5)
const vueTagPrefix = "xvue-init-"; // Prefix for VUE Components - Must not be tagPrefix to avoid loops
// We use only one file as web component for simplicity
// and because codesandbox doesen't work with dynamic requires
const fileName = "MyComp.vue";
const webComponent = require(`../wcs/MyComp.vue`);
const componentName = snakeCase(
camelCase(
// Gets the file name regardless of folder depth
fileName
.split("/")
.pop()
.replace(/.ce/, "")
.replace(/\.\w+$/, "")
)
).replace(/_/, "-");
// store our component
registeredComponents[componentName] = {
component: webComponent.default
};
export default {
init() {
// HTMLParsedElement is a Polyfil Library (NPM Package) to give a clean parsedCallback as the browser
// default implementation has no defined callback when all props and innerHTML is available
class VueCustomElement extends HTMLParsedElement {
// eslint-disable-next-line
constructor() {
super();
}
parsedCallback() {
console.log("Legacy Component Init", this);
if (!this.getAttribute("_innerHTML")) {
this.setAttribute("data-initialized", this.innerHTML);
}
let rootNode = this;
let vueTagName = this.tagName
.toLowerCase()
.replace(tagPrefix, vueTagPrefix);
let compBaseName = this.tagName.toLowerCase().replace(tagPrefix, "");
let compConfig = registeredComponents[compBaseName];
// Optional: Shadow DOM Mode. Can be used by settings __useShadowDom in component vue file
if (compConfig.component.__useShadowDom) {
rootNode = this.attachShadow({ mode: "open" });
document
.querySelectorAll('head link[rel=stylesheet][href*="core."]')
.forEach((el) => {
rootNode.appendChild(el.cloneNode(true));
});
}
if (vueTagName) {
// If we have no type we do nothing
let appNode = document.createElement("div");
// let appNode = rootNode;
appNode.innerHTML +=
"<" +
vueTagName +
">" +
this.getAttribute("data-initialized") +
"</" +
vueTagName +
">"; // #TODO: Some issues with multiple objects being created via innerHTML and slots
rootNode.appendChild(appNode);
// eslint-disable-next-line #typescript-eslint/no-this-alias
const self = this;
function createCustomEvent(name, args) {
return new CustomEvent(name, {
bubbles: false,
cancelable: false,
detail: args.length === 1 ? args[0] : args
});
}
const createEventProxies = (eventNames) => {
const eventProxies = {};
if (eventNames) {
console.log("eventNames", eventNames);
// const handlerName = toHandlerKey(camelCase(name));
eventNames.forEach((name) => {
const handlerName = name;
eventProxies[handlerName] = (...args) => {
this.dispatchEvent(createCustomEvent(name, args));
};
});
}
return eventProxies;
};
const eventProxies = createEventProxies(compConfig.component.emits);
this._props = {};
const app = createApp({
render() {
let props = Object.assign({}, self._props, eventProxies);
// save our attributes as props
[...self.attributes].forEach((attr) => {
let newAttr = {};
newAttr[attr.nodeName] = attr.nodeValue;
props = Object.assign({}, props, newAttr);
});
console.log("props", props);
delete props.dataVApp;
return h(compConfig.component, props);
},
beforeCreate: function () {}
});
// Append only relevant VUE components for this tag
app.component(vueTagPrefix + compBaseName, compConfig.component);
this.vueObject = app.mount(appNode);
console.log("appNode", app.config);
}
}
disconnectedCallback() {
if (this.vueObject) {
this.vueObject.$destroy(); // Remove VUE Object
}
}
adoptedCallback() {
//console.log('Custom square element moved to new page.');
}
}
// Register for all available component tags ---------------------------
// Helper to Copy Classes as customElement.define requires separate Constructors
function cloneClass(parent) {
return class extends parent {};
}
for (const [name, component] of Object.entries(registeredComponents)) {
customElements.define(tagPrefix + name, cloneClass(VueCustomElement));
}
}
};
If I whack this into your render method, it all works fine.
Thus your problem is not Web Components or Events related
document.addEventListener("foo", (evt) => {
console.log("FOOd", evt.detail, evt.composedPath());
});
self.dispatchEvent(
new CustomEvent("foo", {
bubbles: true,
composed: true,
cancelable: false,
detail: self
})
);
addendum after comment:
Your code has bubbles:false - that will never work
This code properly emits an Event
You need to look into how you trigger your proxy code
const createCustomEvent = (name, args = []) => {
return new CustomEvent(name, {
bubbles: true,
composed: true,
cancelable: false,
detail: !args.length ? self : args.length === 1 ? args[0] : args
});
};
const createEventProxies = (eventNames) => {
const eventProxies = {};
if (eventNames) {
eventNames.forEach((name) => {
const handlerName =
"on" + name[0].toUpperCase() + name.substr(1).toLowerCase();
eventProxies[handlerName] = (...args) => {
appNode.dispatchEvent(createCustomEvent(name));
};
});
document.addEventListener("foo",evt=>console.warn("foo!",evt));
appNode.dispatchEvent(createCustomEvent("foo"));
console.log(
"#eventProxies",
eventProxies,
self,
"appnode:",
appNode,
"rootNode",
rootNode
);
}
return eventProxies;
};
I guess you could try to use Vue3's (since 3.2) defineCustomElement
import CustomElement from './some/path/CustomElement.ce.vue'
const CustomElementCE = defineCustomElement(CustomElement)
customElements.define('custom-element', CustomElementCE);
More on that here: Vue web components
I found one possible solution. As stated in the vue docs we can use static event listeners that are passed as props. They have to be prefixed with "on" i.E. onMyevent for event name my-event. Though it works fine - the downside is that I can't pass any arguments as I don't see where to get them from.
One strange thing is that I cant add this event to the instance. Something like self.dispatchEvent() doesn't work when placed inside createEventProxies method. So I have to stick to document.dispatchEvent()
Working CodeSandbox.
const createCustomEvent = (name, args = []) => {
return new CustomEvent(name, {
bubbles: false,
composed: true,
cancelable: false,
detail: !args.length ? self : args.length === 1 ? args[0] : args
});
};
const createEventProxies = (eventNames) => {
const eventProxies = {};
if (eventNames) {
eventNames.forEach((name) => {
const handlerName =
"on" + name[0].toUpperCase() + name.substr(1).toLowerCase();
eventProxies[handlerName] = (...args) => {
document.dispatchEvent(createCustomEvent(name));
};
});
console.log("#eventProxies", eventProxies);
}
return eventProxies;
};

JSX select element does not seem to auto-select (implement 'selected') on option. Am I missing something?

I have a <select> tag which I use to create my own custom <SelectField> component as follows:
export default function SelectField(props) {
/*
PARAMS:
- fieldname (String)
- fieldID (String)
- options (Array([Object, Object...]) [{"option_value": "option_name"}, ...])
- init_value (String "option_value")
*/
const generate_options = () => {
// Function for handling SelectField options
let element_list = [];
element_list.push(
<SelectOption key={0} option_value="" option_text="None" />
);
var count = 1;
if (!Array.isArray(props.options)) {
for (var [value, name] of Object.entries(props.options)) {
element_list.push(
<SelectOption key={count} option_value={value} option_text={name} />
);
count += 1;
}
} else {
props.options.forEach((subject) => {
element_list.push(subject.to_option());
});
}
return element_list;
};
const nameToString = () => {
// Converts props.fieldname into a properly formatted name
if(props.fieldname.indexOf("_")){
var full_field_name = `${props.fieldname.split("_").join(" ")}`;
return `${full_field_name[0].toUpperCase() + full_field_name.slice(1)}`;
};
return `${props.fieldname[0].toUpperCase() + props.fieldname.slice(1)}`;
};
return (
<div className="form-group">
<label htmlFor={props.fieldname}>
{nameToString()}:
</label>
<select
name={`${props.fieldname}`}
id={`${props.fieldID}`}
className="countries"
defaultValue={props?.init_value ? `${props.init_value}` : ""}
>
{generate_options()}
</select>
</div>
);
}
(PS. Don't mind the className - it really SHOULDN'T relevant. But who knows with a beginner like me... DS). Now, I want to use that field to create a <select> tag for Subjects within my web-app (powered by Django), and I created a sub-component for it that looks like this:
export function SubjectSelectField(props) {
const [subjects, setSubjects] = useState([]);
useEffect(() => {
const getSubjects = async () => {
let reqObj = new RequestHandler("/courses/subjects/");
const data = await reqObj.sendRequest();
setSubjects(
data.map((item) => {
return new Subject(item);
})
);
};
getSubjects();
}, []);
console.log({ subjects });
return <SelectField options={subjects} {...props} />;
}
When rendering the page with this component, I get the following console.logs:
{
"subjects": [
{
"id": 6,
"name": "Art"
},
{
"id": 4,
"name": "Biology"
},
{
"id": 5,
"name": "Chemistry"
},
{
"id": 3,
"name": "Geography"
},
{
"id": 2,
"name": "Language"
},
{
"id": 1,
"name": "Mathmatics"
},
{
"id": 7,
"name": "Physics"
},
{
"id": 8,
"name": "Social Studies"
}
]
}
For reference; this is my custom Subject class (ES6):
export class Subject {
constructor({ id, name }) {
this.id = id;
this.name = name;
}
to_option() {
return (
<SelectOption
key={this.id}
option_value={this.id}
option_text={this.name}
/>
);
};
}
And the <SelectOption> component:
export function SelectOption(props) {
return <option value={`${props.option_value}`} >{`${props.option_text}`}</option>;
}
So, the output I expect is for the <SelectField> to automatically assign the selected attribute to the option that has the value of the <SelectField>'s init_value prop.
I use the <SelectField> for another type of field I call <CountrySelectField> and that works just fine. The country I expect to be pre-selected is properly selected as per its init_value.
That component looks like this:
export function CountrySelectField(props) {
return (
<SelectField
init_value={props?.student?.country ? `${props.student.country}` : ""}
{...props}
/>
);
}
As I mentioned, if I pass a value to this component's init_value prop, it executes just as expected and renders with correct option having selected attribute set on it.
The <SubjectSelectField> doesn't render the way I expect - with correct <option> having a selected attribute set.
Why does it not work?
EDIT:
Changed the Subject class to a React functional component:
export function Subject(props){
const id = props.subject.id;
const name = props.subject.name;
console.log(id, name, props.subject);
if(props.option){
return(
<SelectOption
key={id}
option_value={id}
option_text={name}
/>
);
}
return(
<h1>{name} - {id}</h1>
);
}
In order for other things to work, I changed these things too:
export function SubjectSelectField(props) {
// ...
setSubjects(
data.map((item) => {
return <Subject subject={item} />;
})
);
// ...
The option still won't be auto-selected.. I have other fields that works with mostly the same logic (SelectField is used in other places as well and works), but I have clearly messed something up here.
PS.
If you have time to language-police, you definitely have time to look into the question too. Don't you, #halfer? Jeez..
DS.
I am relieved to have solved this problem on my own, and the problem was that I never implemented a check on the SubjectSelecField to render the <select> field ONLY when the Subjects are loaded form the server.
E.g. like this:
export function SubjectSelectField(props) {
const [subjects, setSubjects] = useState([]);
// Here I use 'loaded' as a check variable when rendering.
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const getSubjects = async () => {
let reqObj = new RequestHandler("/courses/subjects/");
try{
const data = await reqObj.sendRequest();
setSubjects(
data.map((item) => {
return <Subject subject={item} />;
})
);
// If the Subjects load properly, set 'loaded' to true.
setLoaded(true);
}
catch(error){
console.log(`Something went wrong when trying load Subjects from server!`);
console.error(error);
// Else we make sure its set to false.
setLoaded(false);
}
};
getSubjects();
}, []);
console.log({ subjects });
// Here, before rendering, we check for the 'loaded' variable
// to make sure the <select> tag doesn't try to load without
// the <option> tags. Otherwise, the 'defaultValue' prop won't work.
if(loaded) return <SelectField init_value={props.init_value} options={subjects} {...props} />;
return <span>Loading Subject list...</span>;
}
Thank you people for trying to look into it at least :)

Update ngrx selector inside ngOnChanges

I have a parent component (B) that is getting data from it's parent input (A)
(C) have is (B) child component.
Inside (B) I'm having a selector that gets data from the store.
export class BComponent implements OnChanges {
#Input() branchId;
ngOnChanges() {
this.selectedDataByBranch$ = this.store.pipe(
select(selectBranchDirections, { branchId: this.branchId, dir: this.selectedDirection })
);
this.selectedDataByBranch$.subscribe(selectedDataByBranch => {
this.trainsDatasets = this.getDatasets(selectedDataByBranch);
this.lineChart.data.datasets = this.trainsDatasets ? this.trainsDatasets : [];
this.lineChart.update();
});
directionChanged(event) {
this.selectedDirection = event;
this.selectedDataByBranch$ = this.store.pipe(
select(selectBranchDirections, { branchId: this.branchId, dir: this.selectedDirection })
);
}
}
directionChanged is the Output event that I get from (C)
The issue this that selectedDataByBranch subscription is not getting the new data update triggered inside selectedDataByBranch$
I have also tried this way
directionChanged(event) {
this.selectedDirection = event;
select(selectBranchDirections, { branchId: this.branchId, dir: this.selectedDirection });
}
What i could suggest is. Turn your parameters into a Subject then merge with the store selection, in your directionChanged(event) method provide value to subject.
So your final code will be something like this:
export class BComponent implements OnChanges {
#Input() branchId;
criterias$= new Subject<{branchId:number,dir:number}>;
ngOnChanges() {
this.selectedDataByBranch$ = this.criterias$.pipe(mergeMap(criteria=> this.store.pipe(
select(selectBranchDirections, { branchId: criteria.branchId, dir: this.searchDirection})
)));
this.selectedDataByBranch$.subscribe(selectedDataByBranch => {
this.trainsDatasets = this.getDatasets(selectedDataByBranch);
this.lineChart.data.datasets = this.trainsDatasets ? this.trainsDatasets : [];
this.lineChart.update();
});
this.criterias$.next({branchId:this.branchId,dir:this.sortDirection}); // init first call
}
directionChanged(event) {
this.selectedDirection = event;
this.criterias$.next({ branchId: criteria.branchId, dir: this.searchDirection}});
);
}
}
This stackblitz tries to materialize what i say.

Manage focusing multiple TextInputs in react-native (more than 10+)

In my React Native application, there will be many TextInputs, and when the user presses the return button, I want react to focus on the next TextInput. Previous stackoverflow threads recommend using refs, but only have examples for 2 TextInputs. What would be the most efficient way to create lets say 30+ refs and handle focusing?
React Native 0.59 w/ regular old js.
I'm using this plugin
https://github.com/gcanti/tcomb-form-native
It allow me to pass inputs as Array, Thus it can be easy manipulate the array and set the focus dynamically.
You can do the following, First install the library
npm install tcomb-form-native
Then import the library and set your input
var t = require('tcomb-form-native');
var Person = t.struct({
name: t.String, // a required string
surname: t.maybe(t.String), // an optional string
age: t.Number, // a required number
rememberMe: t.Boolean // a boolean
});
Now you can customize fields options through an array. Note I just add a key called "next" this will allow me to loop through the array and focus on the next input dynamically in componentDidMount
let options = {
fields: {
name: {
returnKeyType: "next",
next: "surname"
},
surname: {
returnKeyType: "next",
next: "age"
},
age: {
returnKeyType: "next",
next: "rememberMe"
},
rememberMe: {
returnKeyType: "done",
next: "rememberMe"
}
}
};
Now here is the magic, We just need to loop on the array and dynamically add the focus function for each input
Object.keys(options.fields).forEach(function(key) {
if ("next" in options.fields[key]) {
if (key == options.fields[key]["next"]) {
//set the blur in case the input is the last input
options.fields[key].onSubmitEditing = () => {
self.refs["form"].getComponent(key).refs.input.blur();
};
}else{
//set the next focus option
options.fields[key].onSubmitEditing = () => {
self.refs["form"].getComponent(options.fields[key]["next"]).refs.input.focus();
};
}
}
})
self.setState({
options: options
});
and then in the render section you should be able to pass fields and options
<Form
ref="form"
type={Person}
options={this.state.options}
value={this.state.value}
/>
This should work dynamically, without worry about how much inputs do you have.
Hopefully this answer your question!
// i have a form handler class which does it this is its code.
import React, { Component } from "react";
import { getKeyboardType, isInputValid, isSecureTextEntry } from "./Utils";
class Form extends Component {
childReferences = [];
childDetails = [];
/* go through each input and check validation based on its type */
checkValidation = () => {
let isValid = true;
for (let i = 0; i < this.childReferences.length; i++) {
if (
this.childReferences[i].getValue &&
!isInputValid(
this.childReferences[i].getValue(),
this.childDetails[i].type
)
) {
this.childReferences[i].setError &&
this.childReferences[i].setError(true);
isValid = false;
}
}
return isValid;
};
/* collecting user entered values from all inputs */
getValues = () => {
let data = {};
this.childReferences.forEach((item, index) => {
data[this.childDetails[index].identifier] = item.getValue();
});
return data;
};
onSubmitForm = () => {
return this.checkValidation() ? this.getValues() : undefined;
};
refCollector = ref => this.childReferences.push(ref);
collectChildDetails = childProps =>
this.childDetails.push({
identifier: childProps.identifier,
type: childProps.type
});
/* handling onSubmit of each input when user moves to next input from keyboard */
onSubmitEditing = index => ev => {
if (
index < this.childReferences.length - 1 &&
this.childReferences[index + 1].setFocus
) {
this.childReferences[index + 1].setFocus();
}
};
render() {
const wrappedChildrens = [];
React.Children.map(this.props.children, (child, index) => {
if (!child) {
return;
}
/* holding details of input in an array for later user */
this.collectChildDetails(child.props);
/* cloning children and injecting some new props on them */
wrappedChildrens.push(
React.cloneElement(child, {
key: child.props.identifier || `${child.props.type}_${index}`,
ref: this.refCollector,
onSubmitEditing: this.onSubmitEditing(index),
returnKeyType:
index < this.props.children.length - 1 ? "next" : "done",
keyboardType: getKeyboardType(child.props),
secureTextEntry: isSecureTextEntry(child.props)
})
);
});
return wrappedChildrens;
}
}
export default Form;
// this is how its used
<Form ref={ref => (this.formHandler = ref)}>
<MaterialInput
label="Emial address"
error="Enter a valid email"
rightImage={Images.forwardArrow}
type={INPUT_TYPES.EMAIL}
identifier="email"
blurOnSubmit={false}
/>
<MaterialInput
label="Password"
error="Password length must be greater then six"
rightImage={Images.forwardArrow}
type={INPUT_TYPES.PASSWORD}
identifier="password"
/>
</Form>

Bug with passing parameters in ExtReact

In this part of the code passing the properties is fine and they are rendered once on the page:
constructor(props){
super(props);
this.state = {
idOfPortletLocation: props.portletlocationid
};
}
render() {
const text = (
<div>
{this.props.text.description}
{this.state.idOfPortletLocation}
</div>
);
....
as you see from the screenshot:
Proof
But this part of the code does not recognize property idOfPortletLocation:
<Container padding={10} className="containerOfPanel" flex = {1}/*style={{display: 'inline-block', position: 'absolute'}}*/>
<Panel
ref={panel => this.panel = panel}
title= {this.props.text.title}
/*height= {this.props.height}*/
/*minHeight = {this.props.height}*/
tools={[
{type: 'minimize', handler: this.toolHandler},
{type: 'maximize', handler: (e) => this.toolHandler(e, this.state.idOfPortletLocation) },
{type: 'close', handler: this.toolHandler }
]}
resizable={{
edges: "all",
}}
bodyPadding={10}
>
{text}
</Panel>
</Container>
AND:
toolHandler(owner, tool, idOfPortletLocation) {
console.log(tool.config.type);
console.log(idOfPortletLocation); /* Here it is not recognized */
if(tool.config.type.valueOf() == "close"){
console.log("passed");
}
}
handler: (e) => this.toolHandler(e, this.state.idOfPortletLocation)
//here you are passing only two arguments and idOfPorletLocation as second argument
//but in toolHandler there are three arguments and idOfPorletLocation as third argument but was passed as second argument
so correct function should be -
toolHandler(tool, idOfPortletLocation) {
console.log(tool.config.type);
console.log(idOfPortletLocation); /* Here it is now recognized */
if(tool.config.type.valueOf() == "close"){
console.log("passed");
}
Let me know if it works for you)

Categories

Resources