I'm just creating my first custom component, and I'm really struggling with the basics. My component:
<template>
<StackLayout>
<Label :text="title" />
<Label :text="slate.description" />
</StackLayout>
</template>
<script>
var slate;
export default {
name: "SlateComponent",
props:
['slate', 'title'],
data() {
return {
slate: slate,
};
},
}
</script>
This component is to be updated regularly, and occupy a good chunk of the app home page:
<template>
<Page class="Page" actionBarHidden="true" backgroundSpanUnderStatusBar="true" >
<StackLayout>
<StackLayout row="0">
...
</StackLayout>
<StackLayout row="1">
<SlateComponent :title="title" :slate="slate" />
</StackLayout>
...
</Page>
</template>
<script>
...
import SlateComponent from "./SlateComponent";
var slateTitle;
var title;
var gameSlates;
var currentSlate;
var slate;
data() {
return {
events: events,
title: title,
slate: slate,
};
},
async created() {
this.gameSlates = await getGameSlates();
this.currentSlate = this.gameSlates[2];
this.title = this.currentSlate.description;
console.info("The title is: " + this.title);
this.slate = this.currentSlate;
}
};
Result: No matter what I do, no props object passes to the component.
If I comment out the
the app compiles and runs fine, logs currentSlate or its property, description and displays the component, including title.
But, when I include that line, it blows up, with the error: slate is undefined.
(I know that
props:
['slate', 'title'],
is not proper according to the style guide. But I couldn't get the preferred format to work either.)
What am I missing here?
When accessing props anywhere outside of the template, this is required
data() {
return {
slate: slate,
};
}
Should be
data() {
return {
slate: this.slate
};
},
Related
I am new to React TypeScript. I have a problem with the passing state. When I tried to pass the state to a child component and conosle.log(state), I can see the correct object. But, when I tried to do console.log(state.name), I have an error. How can I solve this problem?
App.tsx
export interface Information {
name: string;
age: string;
}
const App: FC = () => {
const [state, setState] = useState<Information | null>({
name: "young",
age: "10",
});
return (
<div className="App">
<div className="header">
<div className="inputContainer">
<input type="text" placeholder="Task.." name="task" />
<input type="number" placeholder="Deadline" name="deadline" />
</div>
<button>Add Task</button>
<div>
<MyForm state={state} />
</div>
</div>
</div>
);
};
Child component
type Props = {
state: ReactNode;
};
const MyForm: FC<Props> = ({ state }: Props) => {
console.log(state.name); // Error
return <div>Hello, {state}</div>;
};
export default MyForm;
Thank you!
The error because you're trying to read the state object inside JSX
return <div>Hello, {state}</div>
Read it like you would with objects instead:
return <div>Hello, {state.name}</div>
Also in your MyForm Component Props, use your Information interface as a type definition instead of ReactNode
export interface Information {
name: string
age: string
}
type Props = {
state: Information
}
Component:
<template>
<div id="fileviewer" class="min-h-full">
<section class="gap-4 mt-4">
<div class="bg-medium-50 w-1/3 p-4">
<FileUpload ></FileUpload>
</div>
<div class="bg-medium-50 w-2/3 p-4">
<FileViewer></FileViewer>
</div>
</section>
</div>
</template>
<script>
import FileUpload from "#/components/FileUpload";
import FileViewer from "#/components/FileViewer";
export default {
name: "FileManager",
components: { FileUpload, FileViewer },
};
</script>
Test:
import { mount } from "#vue/test-utils";
import FileManager from '#/views/FileManager';
describe('FileManager.vue', () =>{
it('should mount', () => {
const wrapper = mount(FileManager, {
global: {
stubs: {
FileUpload: true,
FileViewer: true
}
}
})
expect(wrapper).toBeDefined()
})
})
Does not work for me as per the docs. No special installations. Instead, The framework wants to do the 'import' statements for the child components and then fails because I do not want to mock out 'fetch' for this one component. Any Ideas?
"vue-jest": "^5.0.0-alpha.9"
"#vue/test-utils": "^2.0.0-rc.6"
"vue": "^3.0.0",
Thanks for help.
I. If you want to stub all child components automatically you just can use shallowMount instead of mount.
II. If you want so use mount anyway try to fix your stubs like that:
global: {
stubs: {
FileUpload: {
template: '<div class="file-upload-or-any-class-you-want">You can put there anything you want</div>'
},
FileViewer: {
template: '<div class="file-viewer-or-any-class-you-want">You can put there anything you want</div>'
}
}
}
Or you can define your stubs before tests as I always do. For example:
const FileUploadStub = {
template: '<div class="file-upload-or-any-class-you-want">You can put there anything you want</div>'
}
const FileViewerStub: {
template: '<div class="file-viewer-or-any-class-you-want">You can put there anything you want</div>'
}
And then use stubs in mount or shallowMount:
global: {
stubs: {
FileUpload: FileUploadStub,
FileViewer: FileViewerStub
}
}
I am currently testing one of my children components
I already tested successfully all vuex actions, mutations and getters
When I run this ChangeTitleComonent.spec.js , it passes with 100% coverage.. thanks for feedback
Do I need to write some additional tests when it's 100% covered ? or is this test bad written ?
PARENT
ShoppingListComponent.vue
<template>
<div>
<h2>{{ title }}</h2>
<add-item-component :id='id' #add="addItem"></add-item-component>
<items-component :items="items" :id="id"></items-component>
<div class="footer">
<hr />
<change-title-component :title="title" :id="id"></change-title-component>
</div>
</div>
</template>
<script>
import AddItemComponent from './AddItemComponent'
import ItemsComponent from './ItemsComponent'
import ChangeTitleComponent from './ChangeTitleComponent'
export default {
components: {
AddItemComponent,
ItemsComponent,
ChangeTitleComponent
},
props: ['id', 'title', 'items'],
methods: {
addItem (text) {
this.items.push({
text: text,
checked: false
})
}
}
}
</script>
<style scoped>
.footer {
font-size: 0.7em;
margin-top: 20vh;
}
</style>
CHILDREN
ChangeTitleComponent
<template>
<div>
<em>Change the title of your shopping list here</em>
<input :value="title" #input="onInput({ title: $event.target.value, id: id })"/>
</div>
</template>
<style scoped>
</style>
<script>
import { mapActions } from 'vuex'
export default {
props: ['title', 'id'],
methods: mapActions({ // dispatching actions in components
onInput: 'changeTitle'
})
}
</script>
UNIT TEST
ChangeTitleComponent.spec.js
import Vue from 'vue'
import ChangeTitleComponent from '#/components/ChangeTitleComponent'
import store from '#/vuex/store'
describe('ChangeTitleComponent.vue', () => {
describe('changeTitle', () => {
var component
beforeEach(() => {
var vm = new Vue({
template: '<change-title-component :title="title" :id="id" ref="changetitlecomponent">' +
'</change-title-component></div>',
components: {
ChangeTitleComponent
},
props: ['title', 'id'],
store
}).$mount()
component = vm.$refs.changetitlecomponent
})
it('should change the title', () => {
// check component label text
expect(component.$el.textContent).to.equal('Change the title of your shopping list here ')
// simulate input Enter event
const input = component.$el.querySelector('input')
input.value = 'My New Title'
const enterEvent = new window.Event('keypress', { which: 13 })
input.dispatchEvent(enterEvent)
component._watcher.run()
})
})
})
I am working on an app which was created with the Vue loader's webpack template.
I included testing with Karma as an option when creating the project, so it was all set up and I haven't changed any of the config.
The app is a Github user lookup which currently consists of three components; App.vue, Stats.vue and UserForm.vue. The stats and form components are children of the containing app component.
Here is App.vue:
<template>
<div id="app">
<user-form
v-model="inputValue"
#go="submit"
:input-value="inputValue"
></user-form>
<stats
:username="username"
:avatar="avatar"
:fave-lang="faveLang"
:followers="followers"
></stats>
</div>
</template>
<script>
import Vue from 'vue'
import axios from 'axios'
import VueAxios from 'vue-axios'
import _ from 'lodash'
import UserForm from './components/UserForm'
import Stats from './components/Stats'
Vue.use(VueAxios, axios)
export default {
name: 'app',
components: {
UserForm,
Stats
},
data () {
return {
inputValue: '',
username: '',
avatar: '',
followers: [],
faveLang: '',
urlBase: 'https://api.github.com/users'
}
},
methods: {
submit () {
if (this.inputValue) {
const api = `${this.urlBase}/${this.inputValue}`
this.fetchUser(api)
}
},
fetchUser (api) {
Vue.axios.get(api).then((response) => {
const { data } = response
this.inputValue = ''
this.username = data.login
this.avatar = data.avatar_url
this.fetchFollowers()
this.fetchFaveLang()
}).catch(error => {
console.warn('ERROR:', error)
})
},
fetchFollowers () {
Vue.axios.get(`${this.urlBase}/${this.username}/followers`).then(followersResponse => {
this.followers = followersResponse.data.map(follower => {
return follower.login
})
})
},
fetchFaveLang () {
Vue.axios.get(`${this.urlBase}/${this.username}/repos`).then(reposResponse => {
const langs = reposResponse.data.map(repo => {
return repo.language
})
// Get most commonly occurring string from array
const faveLang = _.chain(langs).countBy().toPairs().maxBy(_.last).head().value()
if (faveLang !== 'null') {
this.faveLang = faveLang
} else {
this.faveLang = ''
}
})
}
}
}
</script>
<style lang="stylus">
body
background-color goldenrod
</style>
Here is Stats.vue:
<template>
<div class="container">
<h1 class="username" v-if="username">{{username}}</h1>
<img v-if="avatar" :src="avatar" class="avatar">
<h2 v-if="faveLang">Favourite Language: {{faveLang}}</h2>
<h3 v-if="followers.length > 0">Followers ({{followers.length}}):</h3>
<ul v-if="followers.length > 0">
<li v-for="follower in followers">
{{follower}}
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'stats',
props: [
'username',
'avatar',
'faveLang',
'followers'
]
}
</script>
<style lang="stylus" scoped>
h1
font-size 44px
.avatar
height 200px
width 200px
border-radius 10%
.container
display flex
align-items center
flex-flow column
font-family Comic Sans MS
</style>
And here is UserForm.vue:
<template>
<form #submit.prevent="handleSubmit">
<input
class="input"
:value="inputValue"
#input="updateValue($event.target.value)"
type="text"
placeholder="Enter a GitHub username..."
>
<button class="button">Go!</button>
</form>
</template>
<script>
export default {
props: ['inputValue'],
name: 'user-form',
methods: {
updateValue (value) {
this.$emit('input', value)
},
handleSubmit () {
this.$emit('go')
}
}
}
</script>
<style lang="stylus" scoped>
input
width 320px
input,
button
font-size 25px
form
display flex
justify-content center
</style>
I wrote a trivial test for UserForm.vue which test's the outerHTML of the <button>:
import Vue from 'vue'
import UserForm from 'src/components/UserForm'
describe('UserForm.vue', () => {
it('should have a data-attribute in the button outerHTML', () => {
const vm = new Vue({
el: document.createElement('div'),
render: (h) => h(UserForm)
})
expect(vm.$el.querySelector('.button').outerHTML)
.to.include('data-v')
})
})
This works fine; the output when running npm run unit is:
UserForm.vue
✓ should have a data-attribute in the button outerHTML
However, when I tried to write a similarly simple test for Stats.vue based on the documentation, I ran into a problem.
Here is the test:
import Vue from 'vue'
import Stats from 'src/components/Stats'
// Inspect the generated HTML after a state update
it('updates the rendered message when vm.message updates', done => {
const vm = new Vue(Stats).$mount()
vm.username = 'foo'
// wait a "tick" after state change before asserting DOM updates
Vue.nextTick(() => {
expect(vm.$el.querySelector('.username').textContent).toBe('foo')
done()
})
})
and here is the respective error when running npm run unit:
ERROR LOG: '[Vue warn]: Error when rendering root instance: '
✗ updates the rendered message when vm.message updates
undefined is not an object (evaluating '_vm.followers.length')
I have tried the following in an attempt to get the test working:
Change how the vm is created in the Stats test to be the same as the UserForm test - same error is returned
Test individual parts of the component, for example the textContent of a div in the component - same error is returned
Why is the error referring to _vm.followers.length? What is _vm with an underscore in front? How can I get around this issue to be able to successfully test my component?
(Repo with all code: https://github.com/alanbuchanan/vue-github-lookup-2)
Why is the error referring to _vm.followers.length? What is _vm with an underscore in front?
This piece of code is from the render function that Vue compiled your template into. _vm is a placeholder that gets inserted automatically into all Javascript expressions when vue-loader converts the template into a render function during build - it does that to provide access to the component.
When you do this in your template:
{{followers.length}}
The compiled result in the render function for this piece of code will be:
_vm.followers.length
Now, why does the error happen in the first place? Because you have defined a prop followers on your component, but don't provide any data for it - therefore, the prop's value is undefined
Solution: either you provide a default value for the prop:
// Stats.vue
props: {
followers: { default: () => [] }, // function required to return fresh object
// ... other props
}
Or you propvide acual values for the prop:
// in the test:
const vm = new Vue({
...Stats,
propsData: {
followers: [/* ... actual data*/]
}
}).$mount()
EDIT: I added this FIDDLE.
I'm a noobie to JSX and I'm trying to (correctly) build an image grid in React using a JSON file of images.
What is the best or "correct" approach for doing this?
My JSON file (updated per Nick's suggestion):
{
"images": [
{
"url": "./images/temp/playlist_tn_01.jpg",
"className": "playlist-tn"
},
{
"url": "./images/temp/playlist_tn_02.jpg",
"className": "playlist-tn"
},
{
"url": "./images/temp/playlist_tn_03.jpg",
"className": "playlist-tn"
}
]
}
My List module (copied from an example online):
var List = React.createClass({
render: function() {
return (
<ul>
{this.props.list.map(function(listValue){
return <li>{listValue}</li>;
})}
</ul>
)
}
});
module.exports = List;
My Updated list container component:
var Playlist = React.createClass({
render() {
let playlistImages = $.getJSON('images/temp/playlist_tn.json', images);
return (
<ReactCSSTransitionGroup transitionName="pagefade" transitionAppear={true} transitionAppearTimeout={500}>
<List list={playlistImages.images} />
</ReactCSSTransitionGroup>
)
}
})
module.exports = Playlist;
UPDATE FIDDLE
If you are rendering the component like this, e.g. passing the image node to the list property:
var mainComponent = React.createClass({
render() {
let jsonFileData = //ajax call to get file here
return (
<List list={jsonFileData.images} />
)
}
})
Then your jsx should read like this, you need to map the properties of the json onto an img element, and you need to use className instead of class as the property name :
var List = React.createClass({
render: function() {
return (
<ul>
{this.props.list.map(function(listValue){
return <li><img url={listValue.url} className={listValue.class} /></li>;
})}
</ul>
)
}
});
module.exports = List;
I would also recommend using arrow functions where possible in your maps, it makes things easier to read:
<ul>
{this.props.list.map( image =>
<li>
<img url={image.url} className={image.class} />
</li>
)}
</ul>
If you change your json class property to be called className and url to be src, you could use the spread operator to set all the props in one go too:
<img {...image} />
Working fiddle