I'm relatively new to React & TypeScript. I'm trying to extend an existing component by making a wrapper around it, but I am having issues trying to add my own values to the properties.
I want it so that the default properties (in a predefined type "TextFieldProps" from the MUI library) carry over, and I can add my own values to it. I'm doing this by making my own type as such:
type PinnableTextFieldProps = TextFieldProps & {
pinned: boolean;
onPin: (newValue: boolean) => void;
};
I then use it as follows:
export function PinnableTextField(props: PinnableTextFieldProps) {
return (
<TextField
{...props}
InputProps={{}}
/>
);
}
This works fine, except that the "pinned" and "onPin" values are copied over to the TextField while they shouldn't be (TextField doesn't know what they are, and an error is printed to the console because of it)
I tried to cast it using ...(props as TextFieldProps) but it still included the properties in the spread.
How would I properly split up the props spread to only include all values inside of the TextFieldProps type, so excluding the 2 values I added?
I hope someone can point me in the right direction!
Many thanks!
You could do something like this:
export function PinnableTextField(props: PinnableTextFieldProps) {
const { pinned, onPin, ...rest } = props; // <= Splitting props into pinned, onPin and all other properties into rest
// Use pinned, onPin here
// Pass the rest of the props down to the TextField
return (
<TextField
{...rest}
InputProps={{}}
/>
);
}
Related
The Problem
We're authoring an npm package containing React components that will be used in various (internal) web sites. There is a problematic npm package dependency that we are forced to use in our react .tsx files, that has these problems:
It doesn't expose any useful types despite having .d.ts files in it... they're empty.
It tries to run when required or imported server-side, instead of waiting until called, so we have to avoid a top-level import and instead do if (window) { const module = require('package-name') } and then use it inside that block only.
It is a frequent source of errors so everything in that library needs to be run inside of a try ... catch block.
Well, At Least We Have Types
We have already created our own types file which addressed problem #1:
// problematic-package-types.d.ts
declare module 'problematic-package' {
function doErrorProneButNecessaryThing(
foo: Record<string, unknown>,
bar: string
): void
}
The Needed Solution
The long term solution is to fix this problematic library and we're looking into how to get that done (but it's not in our direct control).
In the short term, though, we need a solution now.
Note that we are configuring dynamic requires in our npm package bundler to import them only at use-time, not treating them like other imports/requires. As our package is consumed inside other applications, we don't have full control over how that application bundling works or when the components are required, so our components may end up being required server-side when they shouldn't, and we have to tolerate that. We're still learning about some aspects of this.
My Wild (But Failed) Stab
My goal is to do something more DRY like this, where we solve all three problems of strong typing, detecting server-side execution & doing nothing, and adding error handling:
// hoping to leverage our module declaration above without importing anything
import type * as ProblematicPackage from 'problematic-package'
import wrapProblematicRequire from '../utils/my-sanity-preserving-module'
const wrappedProblematicPackage = wrapProblematicRequire<ProblematicPackage>()
// then later...
const foo: Record<string, unknown> = { property1: 'yes', property2: false }
const bar = 'yodeling'
wrappedProblematicPackage.invokeIfWindowReady(
'doErrorProneButNecessaryThing',
foo,
bar
)
However, TypeScript doesn't like the import type which unfortunately makes sense:
Cannot use namespace 'ProblematicPackage' as a type.
The Plea
How do I get the type information we've placed into problematic-package-types.d.ts to use as desired?
Or ANYTHING else. Honestly, I'm open to whatever, no matter how crude or hacky, so long as we get some clarity and reliability at call sites, with full type information as described. Suggestions/advice?
Full Details
Here is the full implementation of the wrapProblematicRequire function. I haven't tested it. It's probably awful. I'm sure it could be far better but I don't have time to get this helper module super clean right now. (My attempt to handle function type information isn't quite right.)
type Func = (...args: any[]) => any
type FunctionNames<T, TName extends keyof T> = T[TName] extends Func ? TName : never
type FunctionNamesOf<T> = FunctionNames<T, keyof T>
const wrapProblematicRequire = <T>(packageName: string) => ({
invokeIfWindowReady<TName extends FunctionNamesOf<T>>(
name: T[TName] extends Func ? TName : never,
...args: T[TName] extends Func ? Parameters<T[TName]> : never
): T[TName] extends Func ? ReturnType<T[TName]> : never {
if (!window) {
// #ts-ignore
return undefined
}
try {
// #ts-ignore
return require(packageName)[name] as T[TName](...args)
} catch (error: unknown) {
// ToDo: Log errors
// #ts-ignore
return undefined
}
}
})
export default wrapProblematicRequire
P.S. await import('problematic-package') didn't seem to work. Yes, problems abound.
Cannot use namespace 'ProblematicPackage' as a type.
Well, you can get the typeof that namespace, which seems to be what you want.
To test this, I setup the following:
// problem.js
export function doErrorProneButNecessaryThing(n) {
return n;
}
export function doErrorProneButNecessaryThing2(s) {
return s;
}
console.log('did side effect');
// problem.d.ts
export function doErrorProneButNecessaryThing(n: number): number;
export function doErrorProneButNecessaryThing2(s: string): string;
And now you can do:
import type * as ProblemNs from './problem';
type Problem = typeof ProblemNs;
// works
type A = Problem['doErrorProneButNecessaryThing'] // type A = (n: number) => number
Then the wrapProblematicRequire function just takes the name of the function as a generic, pulls the args for it, and pulls the return type.
const wrapProblematicRequire = <TName extends FunctionNamesOf<Problem>>(
name: TName,
...args: Parameters<Problem[TName]>
): ReturnType<Problem[TName]> | undefined => {
if (!window) return;
const problem = require('./problem'); // type is any, but types are enforced above
try {
return problem[name](...args);
} catch (err) {
console.log('error!');
}
};
Here require('./problem') returns the any type, but the generics keep everything key safe as long as typeof ProblemNs can be trusted.
Now to test that:
console.log('start');
const result: number = wrapProblematicRequire(
'doErrorProneButNecessaryThing',
123
);
console.log('end');
Which logs:
start
did side effect
end
Which seems to work!
Codesandbox
The lit-element documentation describes conditional rendering via (condition ? a : b). I was wondering how to use that to render one of multiple pages, f.e. in combination with mwc-tab-bar from Googles material web components.
My current solution is something like this:
render() {
... other stuff ...
${this.selectedPage === 0 ? html`
<div>
...
</div>
` : html``}
${this.selectedPage === 1 ? html`
<div>
...
</div>
` : html``}
... further pages ...
}
I don't like the :html`` part but is that how it's meant to be?
Use more simple code like this.
constructor(){
super();
// don't forget add `prop` and `selectedPage` to `static get properties()`
this.prop = 1;
}
render() {
return this.getPage(this.selectedPage);
}
getPage(num){
switch(num){
default:
case 1:
return html`<div>P${this.prop}<div>`;
case 2:
return html`<div>P2<div>`;
}
}
There are multiple ways of achieving this, your solution is one, but as you mention, it's not the prettiest
One way you could modularize this somewhat is using an object/array and render functions, basically the idea is this:
First, define render functions for each page (this can be on the same file or on different files):
const page0Renderer = (context) => {
return html`<section>${context.someData}</section>`;
};
Then, you could define an object that has a match between the page identifiers and their respective functions, you are using numbers so the sample below uses numbers:
const pageRenderers = {
'0': page0Renderer,
'1': page1Renderer,
'2': page2Renderer,
// etc
};
And in your main render function you could use all these like this:
render() {
return html`
${pageRenderers[`${this.selectedPage}`](this)}
`;
}
This would basically call the render function that matches the selected page and send it a reference to the main web component so that you can access its properties.
Then again, this approach also has its flaws and I wouldn't really recommend it much if you need your child templates to be complex.
In that case, instead of rendering functions you probably would be better off creating other components for each view and that way you could also do some lazy loading and so on.
For that kind of approach, you might want to check out routers like vaadin router which help you both with routing and changing which component gets displayed accordingly
I am trying to use the up-to-date method to get a value from a Select element via React but I seem to be getting all kinds of errors. I originally used string refs, but it seems like that is now considered legacy.
class Base extends React.Component<any, any> {
constructor(props){
super(props);
this.candInput = React.createRef();
this.state = {name: '', ns:'', svc: '', base:'', cand:[]};
}
handleAddCand = (e) => {
if(!this.state.cand.includes(this.candInput.current.value) &&
this.state.base !== (this.candInput.current.value)){
this.setState((prevState) => ({
cand: [...prevState.cand, this.candInput.current.value],
}));
}
}
And the returned components that use the ref is placed like:
<Select
id="ns-select"
labelText="Namespace"
helperText="..."
ref={this.candInput}
/>
<Button
onClick={this.handleAddCand}
>
Add
</Button>
For some reason I keep getting errors that claim that candInput doesn't exist? The compiler err msg is:
Property 'candInput' does not exist on type 'Base'.
And the error that ref is not a property available for this particular element. The element is a imported Carbon Component, but I can't tell if that's the core issue. The error message is:
Property 'ref' does not exist on type 'IntrinsicAttributes & SelectProps & { children?: ReactNode; }'.
My react version is 16.12 and the carbon component version is 7.10, so I definitely have updated modules. Any pointers or help would be greatly appreciated!
I have a very simple Angular2 app running locally. I'm using a service to send an instance of an object to a webservice API. The API validates JSON against a schema, and ID's must be numbers (i.e. NOT quoted in the JSON).
My problem is that when I try to send the object to the webservice, my ID field has quotes around it, even though it's typed to be a number in Typescript.
The behaviour is only observed when the nameproperty of the object contains "special characters".
I've tested and found that it doesn't seem to be the JSON.stringify I use — please see code below.
The app is written in Typescript.
Unit class:
export class Unit {
id: number; // This is the problem child
name: string; // This is the string that can contain special characters
short: string;
triggers_plural: number;
is_headline: boolean;
}
Method to save:
My code for saving an instance of Unit to the webservice:
updateUnit(unit: Unit): Promise<Unit>
{
var objSend = {unit_data: [unit]}; // Webservice expects an array of units
console.log(objSend.unit_data[0].id === 2); // Yields false when ID is 2
console.log(objToReturn); // Logs ID to verify it is 2 when testing
// Code for actual request
return this.http.put(`${this.unitUrl}/${unit.id}`, JSON.stringify(objSend),{headers:this.headers})
.toPromise()
.then(()=> unit)
.catch(this.handleError);
}
When running the code and calling the method, the console will log that the ID is NOT equal when the Unit object's name property contains special characters.
Example without special characters (no problem, id is a number):
Example WITH special characters (eek! Id is a string!):
The updateUnit method is called from my unit-detail component where you can edit a unit:
export class UnitDetailComponent implements OnInit {
unit: Unit; // this.unit later on
constructor(
private unitService: UnitService,
private route: ActivatedRoute
){}
ngOnInit(): void
{
this.route.params.forEach((params: Params) => {
let id = +params['id']; // The routing will give id to look for
this.unitService.getUnit(id)
.then(unit => this.unit = unit); // Here the unit is instanciated in the first place
});
}
save(): void
{
this.unitService.updateUnit(this.unit).then(this.goBack); // Here is the call to updateUnit method
}
}
It's bound to an input in the template:
<div *ngIf="unit">
<div>
<label>Edit unit</label>
<div>
<input type="text" [(ngModel)]="unit.name" />
</div>
</div>
<button class="btn btn-default" type="button" (click)="save()">Save</button>
</div>
Maybe the problem arises already when the two-way data binding is filling in the name property when you write something in the <input> but I don't understand how the type of the id can change?
Link to github repos of the whole project: https://github.com/djoike/ng2-cookbook/tree/master-so
A small solution is to cast the field to any , and then convert it to a number using parseInt(), I faced a similar problem just today
for more info about casting, check the Type assertions section here
Trying to use a property to configure a WinJS control from within Angular2, so far I couldn't find a solution, e.g. this code below is throwing 'Can't bind to 'dataWinOptions' since it isn't a known property of the '' element'.
#View({
template: `<div id="rating" data-win-control='WinJS.UI.Rating' [data-win-options]='jsonRating'></div>`
})
class MyRating {
rating: number;
get jsonRating() {
return '{averageRating: ' + this.rating + '}';
}
constructor() {
this.rating = 1.5;
}
}
Any hint?
#ericdes about your last comment I think this would be the best option. Assuming you have Nth WinJS controls
Consider the following code. I'm specifying differents values for the averageRating property in options.
<winjs-control [options]="{averageRating: '1.5', someMoreOptions : 'x'}"></winjs-control>
<winjs-control [options]="{averageRating: '1.4', differentOptionsForThisOne :'Z'}"></winjs-control>
<winjs-control [options]="{averageRating: '1.3'}"></winjs-control>
<winjs-control [options]="{averageRating: '1.2'}"></winjs-control>
<winjs-control [options]="{averageRating: '1.1'}"></winjs-control>
// more and more...
The component will read this options property and will pass it to the view. Forget about the directive, it isn't necessary after all.
We pass options through attr.data-win-options since it isn't a property of div but an attribute.
#Component({
selector : 'winjs-control',
properties : ['options']
})
#View({
template : `<div data-win-control="WinJS.UI.Rating" [attr.data-win-options]="jsonRating"></div>`,
})
class WinJSComponent implements OnInit, AfterViewInit {
constructor() {}
// We specify onInit so we make sure 'options' exist, at constructor time it would be undefined
// And we stringify it or otherwise it will pass an object, we need to convert it to a string
onInit() {
this.jsonRating = JSON.stringify(this.options);
}
// We process WinJS after view has been initialized
// this is necessary or 'data-win-options' won't be fully processed
// and it will fail silently...
afterViewInit() {
WinJS.UI.processAll();
}
}
Here's a plnkr for this case.
That's one option and IMHO I think this is the easiest one. Another one, having the same HTML content, would be to communicate the parent with its children and I haven't tested your case with that approach.