I am not using React.
I am using Stenciljs.
I have the following .tsx file:
export class MyComponent {
#Prop() message: string;
render() {
return (<div>{this.message}</div>);
}
}
I want to do this instead:
import myTemplate from '../my-template.??';
export class MyComponent {
#Prop() message: string;
render() {
return (myTemplate);
}
}
with ../my-template.?? containing:
<div>{this.message}</div>
Is it possible and how ? Thanks in advance for any help :)
Yes, you can absolutely do this, there are just a couple of things you need to tidy up:
Main file
import { Template } from '../template'; // No need for file extension but we're using a named export so we need the curly braces around 'Template'
export class MyComponent {
#Prop() message: string;
render() {
return ( // You don't technically need the parentheses here as you're just returning one thing
<Template /> // When outputting an imported component, it goes in angle brackets and the backslash closes it like an HTML element
)
}
}
Template
import React from 'react'; // template needs React
export const Template = () => { // defining the export in this way is known as a named export
return (
<p>A message here</p>
)
}
Okay, so that's going to get you a message output which is from your template. However, you were asking about passing a message to that template for it to output. That's totally easy as well - you just need to get some props in there. Here is the modified version of the above:
Main file
import { Template } from '../template';
export class MyComponent {
#Prop() message: string;
render() {
return (
<Template messageToOutput={message} /> // The first argument is the name of the prop, the second is the variable you defined above
)
}
}
Template
import React from 'react';
export const Template = (props) => { // props are received here
return (
<p>{props.messageToOutput}</p> // props are used here
)
}
That's how you pass data around in React - hope that helps!
Related
I'm using Storybook 6.5.9 to render web components (made with Stencil). I have several of them working correctly but now I'm creating a story for a new component that can receive a property that is an array of objects.
The component would be something like this:
import {Component, Host, h, Prop} from '#stencil/core';
#Component({
tag: 'test-component',
})
export class TestComponent {
/**
* Array of options for the group.
*/
#Prop() items?: String[] = [];
render() {
return (
<Host>
{
this.items.map(item => (
<h1>{item}</h1>
))
}
</Host>
)
}
}
And this is the story:
import { Story, Meta } from '#storybook/html';
export default {
title: 'Components/TestComponent',
parameters: {
options: {
showPanel: true
},
}
} as Meta;
const Template: Story = (args) => {
return `
<test-component items="${['some', 'thing']}">
</test-component>
`;
};
export const TestComponent: Story = Template.bind({});
I have tried setting the items property to a string but the component never gets anything, it's always an empty array.
I'm not getting any errors either in the console. I'm definitely doing something wrong but I don't know what it is... I've been using several other data types for those properties (boolean, string, numbers...) but this is the first time I'm using objects/arrays and I'm not able to get it to work.
Any help will be highly appreciate it.
Thanks!
Properties are passed in as strings in HTML.
Use JSON.parse(this.items) in your render() method in case this.items is not an array:
import { Component, Host, h, Prop } from '#stencil/core';
#Component({
tag: 'my-component',
})
export class MyComponent {
/**
* Array of options for the group.
*/
#Prop() items?: string | string[] = '';
render() {
return (
<Host>
{(Array.isArray(this.items) ? this.items : JSON.parse(this.items)).map(item => (
<h1>{item}</h1>
))}
</Host>
);
}
}
For that to work, you need to pass in your items as valid JSON, meaning you have to use single attribute quotes and double quotes for the "strings" in the "array":
const Template: Story = (args) => `
<test-component items='${["some", "thing"]}'>
</test-component>
`;
If you are receiving [object Object] in your component, try to stringify your prop value before passing:
const Template: Story = (args) => `
<test-component items='${JSON.stringify(["some", "thing"])}'>
</test-component>
`;
You can not set arrays or objects as properties via HTML (only in JSX/TSX). You have two options:
pass them as a JSON string and parse them back into an array/object; you’d basically have to type them as a string in your component, and then cast the type after parsing them
set the property via a script; there’s two options, depending on whether or not the option is required or not.
<test-component></test-component>
<script>
document.querySelector("test-component").items = ["foo"]
</script>
or if the prop is required:
<script>
const testComponent = document.createElement("test-component")
testComponent.items = ["foo"]
document.body.appendChild(testComponent)
</script>
While rewriting my VueJs project in typescript, I came across a TypeScript error.
This is a part of the component that has a custom v-model.
An input field in the html has a ref called 'plate' and I want to access the value of that. The #input on that field calls the update method written below.
Typescript is complaining that value does not exist on plate.
#Prop() value: any;
update() {
this.$emit('input',
plate: this.$refs.plate.value
});
}
template:
<template>
<div>
<div class="form-group">
<label for="inputPlate" class="col-sm-2 control-label">Plate</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="inputPlate" ref="plate" :value="value.plate" #input="update">
</div>
</div>
</div>
</template>
You can do this:
class YourComponent extends Vue {
$refs!: {
checkboxElement: HTMLFormElement
}
someMethod () {
this.$refs.checkboxElement.checked
}
}
From this issue: https://github.com/vuejs/vue-class-component/issues/94
Edit - 2021-03 (Composition API)
Updating this answer because Vue 3 (or the composition API plugin if you're using Vue 2) has some new functions.
<template>
<div ref="root">This is a root element</div>
</template>
<script lang="ts">
import { ref, onMounted, defineComponent } from '#vue/composition-api'
export default defineComponent({
setup() {
const root = ref(null)
onMounted(() => {
// the DOM element will be assigned to the ref after initial render
console.log(root.value) // <div>This is a root element</div>
})
return {
root
}
}
})
</script>
Edit - 2020-04:
The vue-property-decorator library provides #Ref which I recommend instead of my original answer.
import { Vue, Component, Ref } from 'vue-property-decorator'
import AnotherComponent from '#/path/to/another-component.vue'
#Component
export default class YourComponent extends Vue {
#Ref() readonly anotherComponent!: AnotherComponent
#Ref('aButton') readonly button!: HTMLButtonElement
}
Original Answer
None of the above answers worked for what I was trying to do. Adding the following $refs property wound up fixing it and seemed to restore the expected properties. I found the solution linked on this github post.
class YourComponent extends Vue {
$refs!: {
vue: Vue,
element: HTMLInputElement,
vues: Vue[],
elements: HTMLInputElement[]
}
someMethod () {
this.$refs.<element>.<attribute>
}
}
son.vue
const Son = Vue.extend({
components: {},
props: {},
methods: {
help(){}
}
...
})
export type SonRef = InstanceType<typeof Son>;
export default Son;
parent.vue
<son ref="son" />
computed: {
son(): SonRef {
return this.$refs.son as SonRef;
}
}
//use
this.son.help();
This worked for me: use
(this.$refs.<refField> as any).value or (this.$refs.['refField'] as any).value
Avoid using bracket < > to typecast because it will conflict with JSX.
Try this instead
update() {
const plateElement = this.$refs.plate as HTMLInputElement
this.$emit('input', { plate: plateElement.value });
}
as a note that I always keep remembering
Typescript is just Javascript with strong typing capability to ensure type safety. So (usually) it doesn't predict the type of X (var, param, etc) neither automatically typecasted any operation.
Also, another purpose of the typescript is to make JS code became clearer/readable, so always define the type whenever is possible.
Maybe it will be useful to someone. It looks more beautiful and remains type support.
HTML:
<input ref="inputComment" v-model="inputComment">
TS:
const inputValue = ((this.$refs.inputComment as Vue).$el as HTMLInputElement).value;
In case of custom component method call,
we can typecast that component name, so it's easy to refer to that method.
e.g.
(this.$refs.annotator as AnnotatorComponent).saveObjects();
where AnnotatorComponent is class based vue component as below.
#Component
export default class AnnotatorComponent extends Vue {
public saveObjects() {
// Custom code
}
}
With Vue 3 and the Options API, this is what worked for me:
<script lang="ts">
import {defineComponent} from 'vue';
export default defineComponent({
methods: {
someAction() {
(this.$refs.foo as HTMLInputElement).value = 'abc';
},
},
});
</script>
The autocomplete doesn't bring the foo property from $refs because it's defined in the template, and apparently there's no information inferred from it.
However, once you force the casting of .foo to the HTML element type, everything works from there on, so you can access any element property (like .value, in the example above).
Make sure to wrap your exports with Vue.extend() if you are converting your existing vue project from js to ts and want to keep the old format.
Before:
<script lang="ts">
export default {
mounted() {
let element = this.$refs.graph;
...
After:
<script lang="ts">
import Vue from "vue";
export default Vue.extend({
mounted() {
let element = this.$refs.graph;
...
I found a way to make it work but it is ugly in my opinion.
Feel free to give other/better suggestions.
update() {
this.$emit('input', {
plate: (<any>this.$refs.plate).value,
});
}
I spent a LONG time trying to find an answer to this using Vue 3, TypeScript with class components and (as it happens, although not relevant to this) TipTap. Found the answer from bestRenekton above which finally solved it, but it needed tweaking. I'm pretty sure this is TypeScript specific.
My child component has this at the start:
export default class WhealEditor extends Vue {
It includes this method (the one I want to call from the parent):
doThis(what: string) {
console.log('Called with ' + what)
}
And this right at the end:
export type EditorRef = InstanceType<typeof WhealEditor>
</script>
So this announces to any consumer of the child component that it can access it using the variable EditorRef. The parent component includes the child component in the template:
<WhealEditor ref="refEditor" />
The parent component then imports ref, and the child component and the exposed object:
import { ref } from 'vue'
import WhealEditor, { EditorRef } from './components/WhealEditor.vue'
I then have a method to get this object:
getEditor(): EditorRef {
// gets a reference to the child component
return this.$refs.refEditor as EditorRef
}
Finally, I can handle events - for example:
processButton(msg: string) {
// runs method in child component
this.getEditor().doThis(msg)
Like everything else to do with client script, it's so much harder than I expected!
While rewriting my VueJs project in typescript, I came across a TypeScript error.
This is a part of the component that has a custom v-model.
An input field in the html has a ref called 'plate' and I want to access the value of that. The #input on that field calls the update method written below.
Typescript is complaining that value does not exist on plate.
#Prop() value: any;
update() {
this.$emit('input',
plate: this.$refs.plate.value
});
}
template:
<template>
<div>
<div class="form-group">
<label for="inputPlate" class="col-sm-2 control-label">Plate</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="inputPlate" ref="plate" :value="value.plate" #input="update">
</div>
</div>
</div>
</template>
You can do this:
class YourComponent extends Vue {
$refs!: {
checkboxElement: HTMLFormElement
}
someMethod () {
this.$refs.checkboxElement.checked
}
}
From this issue: https://github.com/vuejs/vue-class-component/issues/94
Edit - 2021-03 (Composition API)
Updating this answer because Vue 3 (or the composition API plugin if you're using Vue 2) has some new functions.
<template>
<div ref="root">This is a root element</div>
</template>
<script lang="ts">
import { ref, onMounted, defineComponent } from '#vue/composition-api'
export default defineComponent({
setup() {
const root = ref(null)
onMounted(() => {
// the DOM element will be assigned to the ref after initial render
console.log(root.value) // <div>This is a root element</div>
})
return {
root
}
}
})
</script>
Edit - 2020-04:
The vue-property-decorator library provides #Ref which I recommend instead of my original answer.
import { Vue, Component, Ref } from 'vue-property-decorator'
import AnotherComponent from '#/path/to/another-component.vue'
#Component
export default class YourComponent extends Vue {
#Ref() readonly anotherComponent!: AnotherComponent
#Ref('aButton') readonly button!: HTMLButtonElement
}
Original Answer
None of the above answers worked for what I was trying to do. Adding the following $refs property wound up fixing it and seemed to restore the expected properties. I found the solution linked on this github post.
class YourComponent extends Vue {
$refs!: {
vue: Vue,
element: HTMLInputElement,
vues: Vue[],
elements: HTMLInputElement[]
}
someMethod () {
this.$refs.<element>.<attribute>
}
}
son.vue
const Son = Vue.extend({
components: {},
props: {},
methods: {
help(){}
}
...
})
export type SonRef = InstanceType<typeof Son>;
export default Son;
parent.vue
<son ref="son" />
computed: {
son(): SonRef {
return this.$refs.son as SonRef;
}
}
//use
this.son.help();
This worked for me: use
(this.$refs.<refField> as any).value or (this.$refs.['refField'] as any).value
Avoid using bracket < > to typecast because it will conflict with JSX.
Try this instead
update() {
const plateElement = this.$refs.plate as HTMLInputElement
this.$emit('input', { plate: plateElement.value });
}
as a note that I always keep remembering
Typescript is just Javascript with strong typing capability to ensure type safety. So (usually) it doesn't predict the type of X (var, param, etc) neither automatically typecasted any operation.
Also, another purpose of the typescript is to make JS code became clearer/readable, so always define the type whenever is possible.
Maybe it will be useful to someone. It looks more beautiful and remains type support.
HTML:
<input ref="inputComment" v-model="inputComment">
TS:
const inputValue = ((this.$refs.inputComment as Vue).$el as HTMLInputElement).value;
In case of custom component method call,
we can typecast that component name, so it's easy to refer to that method.
e.g.
(this.$refs.annotator as AnnotatorComponent).saveObjects();
where AnnotatorComponent is class based vue component as below.
#Component
export default class AnnotatorComponent extends Vue {
public saveObjects() {
// Custom code
}
}
With Vue 3 and the Options API, this is what worked for me:
<script lang="ts">
import {defineComponent} from 'vue';
export default defineComponent({
methods: {
someAction() {
(this.$refs.foo as HTMLInputElement).value = 'abc';
},
},
});
</script>
The autocomplete doesn't bring the foo property from $refs because it's defined in the template, and apparently there's no information inferred from it.
However, once you force the casting of .foo to the HTML element type, everything works from there on, so you can access any element property (like .value, in the example above).
Make sure to wrap your exports with Vue.extend() if you are converting your existing vue project from js to ts and want to keep the old format.
Before:
<script lang="ts">
export default {
mounted() {
let element = this.$refs.graph;
...
After:
<script lang="ts">
import Vue from "vue";
export default Vue.extend({
mounted() {
let element = this.$refs.graph;
...
I found a way to make it work but it is ugly in my opinion.
Feel free to give other/better suggestions.
update() {
this.$emit('input', {
plate: (<any>this.$refs.plate).value,
});
}
I spent a LONG time trying to find an answer to this using Vue 3, TypeScript with class components and (as it happens, although not relevant to this) TipTap. Found the answer from bestRenekton above which finally solved it, but it needed tweaking. I'm pretty sure this is TypeScript specific.
My child component has this at the start:
export default class WhealEditor extends Vue {
It includes this method (the one I want to call from the parent):
doThis(what: string) {
console.log('Called with ' + what)
}
And this right at the end:
export type EditorRef = InstanceType<typeof WhealEditor>
</script>
So this announces to any consumer of the child component that it can access it using the variable EditorRef. The parent component includes the child component in the template:
<WhealEditor ref="refEditor" />
The parent component then imports ref, and the child component and the exposed object:
import { ref } from 'vue'
import WhealEditor, { EditorRef } from './components/WhealEditor.vue'
I then have a method to get this object:
getEditor(): EditorRef {
// gets a reference to the child component
return this.$refs.refEditor as EditorRef
}
Finally, I can handle events - for example:
processButton(msg: string) {
// runs method in child component
this.getEditor().doThis(msg)
Like everything else to do with client script, it's so much harder than I expected!
In the Creating a component section of React Typescript Starter example, Creating a component, there is a basic React component in Typescript:
// src/components/Hello.tsx
import * as React from 'react';
export interface Props {
name: string;
enthusiasmLevel?: number;
}
function Hello({ name, enthusiasmLevel = 1 }: Props) {
if (enthusiasmLevel <= 0) {
throw new Error('You could be a little more enthusiastic. :D');
}
return (
<div className="hello">
<div className="greeting">
Hello {name + getExclamationMarks(enthusiasmLevel)}
</div>
</div>
);
}
export default Hello;
// helpers
function getExclamationMarks(numChars: number) {
return Array(numChars + 1).join('!');
}
I am new to Typescript. It seems that the interface Props is used by Typescript to do props type checks (similar to what the Proptypes npm package does). So the question is:
If I am already using this kind of Typescript
interface syntax do to props type check, do I still need to use
Proptypes package like this in the same component?
import PropTypes from 'prop-types';
Hello.propTypes = {
name: PropTypes.string,
enthusiasmLevel: PropTypes.number
};
Besides, why does here use export interface? what is the purpose
of exporting the interface Props? Is it compulsory?
Firstly I recommend declaring your components the ES6 way
const Hello: React.FC<IHello> = ({ name, enthusiasmLevel = 1 }) => {}
Your interface defines the contract of your component / The accepted parameters
export interface IHello {
name: string;
enthusiasmLevel?: number;
}
You are exporting this, so you can import your interface from other files / components which want to make use of the Hello component. For example you can use your Hello component like so from another component:
const props: IHello = {
name: "John",
enthusiamsLevel: 5
}
<Hello {...props} />
If I am already using this kind of Typescript interface syntax do to props type check, do I still need to use Proptypes in the same component?
You always want type strong definitions in TypeScript. So when declaring your prop variable in another component, you don't want to do const props: any = {
If you decide to change your interface declaration for this component later on, you would be forced to update all your references which uses this interface. - You might want to require 1 more prop variable and in that case you would want to update your usages of this interface. If you are not used to TypeScript this can seem quite hideous at first - but the benefit of always having strong type definitions will show over time. Especially when you update your type definitions.
While I searched & read around, I am still a newbie (or to be exact, a rusty, part-time JS developer) and my question may be awfully simple.
I am trying to convert an existing JS project to TypeScript. I am struggeling to understand the combination of connect-redux-decorator, JS shorthand notation and arrow nomenclature in one single line:
#connect(({session: { loggedIn }}) => ({loggedIn}), {establishConnection})
I would be very grateful, if someone could translate it to an "export default connect" approach or just a functions/classes notation.
In the following code the line results in this TypeScript error message:
[ts]
Unable to resolve signature of class decorator when called as an expression.
Type 'ComponentClass<Pick<{}, never>, any> & { WrappedComponent: ComponentType<{}>; }' is
not assignable to type 'typeof AuthenticatedApp'.
Type 'Component<Pick<{}, never>, any, any>' is not assignable to type 'AuthenticatedApp'.
Property 'componentDidMount' is optional in type
'Component<Pick<{}, never>, any, any>' but required in type 'AuthenticatedApp'.`
The code:
import * as React from 'react';
import { connect } from 'react-redux'
import Connecting from '../components/Connecting'
import { establishConnection } from '../actions/login'
#connect(({session: { loggedIn }}) => ({loggedIn}), {establishConnection})
export default class AuthenticatedApp extends React.Component {
componentDidMount() {
this.props.establishConnection()
}
componentWillReceiveProps(nextProps) {
if(!nextProps.loggedIn) {
this.props.establishConnection()
}
}
render() {
const { loggedIn, children } = this.props
const appContent = loggedIn ? children : <Connecting />
return (
<div>
{appContent}
</div>
)
}
}
While the error message itself may be also solved totally differently I wanted to replace the decorator by "plain" code, hoping that it solves the issue.
My try looked liked something like this (totally wrong, I know):
function mapStateToProps(loggedIn) {
return {loggedIn} = session: { loggedIn };
}
...
class AuthenticatedApp extends React.Component {
...
export default connect(mapStateToProps, establishConnection)(AuthenticatedApp);
I am using
Visual Studio Code (1.26.1)
"#types/react": "^16.4.13"
"#types/react-dom": "^16.0.7"
"typescript": "^3.0.3"
Thank you!
HerrB92
As a starting point, you would take the exact same connect call expression and apply it to the component manually instead of using a decorator:
export default connect(({session: { loggedIn }}) => ({loggedIn}), {establishConnection})(AuthenticatedApp);
(And remove export default from the class.) Now you'll get a (not very clear) error because TypeScript can't infer the parameter type of the mapStateToProps function. You can annotate it like this:
export default connect(({session: { loggedIn }}: {session: {loggedIn: boolean}}) => ({loggedIn}), {establishConnection})(AuthenticatedApp);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(Maybe you have a type already defined in your codebase that contains {session: {loggedIn: boolean}} or even more.)
Alternatively, you can pull out the mapStateToProps function, leaving it as an arrow function:
const mapStateToProps =
({session: { loggedIn }}: {session: {loggedIn: boolean}}) => ({loggedIn});
export default connect(mapStateToProps, {establishConnection})(AuthenticatedApp);
or convert it to a traditional function definition:
function mapStateToProps({session: { loggedIn }}: {session: {loggedIn: boolean}}) {
return {loggedIn};
}
export default connect(mapStateToProps, {establishConnection})(AuthenticatedApp);
Ultimately, to compile the component successfully, you'll need to specify its props type, something like this:
class AuthenticatedApp extends React.Component<{establishConnection: () => void, loggedIn: boolean}> { ... }
but that's a separate issue from the connect.