I'm trying to add Normalize.css as global and use emotion for my CSS Modules.
First my .babelrc
{
"presets": [
["env", {
"modules": false,
"useBuiltIns": true
}],
"next/babel"
],
"plugins": [
"syntax-dynamic-import",
"transform-runtime",
"transform-decorators-legacy",
"transform-class-properties",
"transform-object-rest-spread",
"es6-promise",
["module-resolver", {
"root": ["./src"],
"alias": {
"styles": "./styles",
"assets": "./assets",
},
"cwd": "babelrc"
}],
["inline-import", { "extensions": [".css"] } ],
["emotion", { "inline": true }]
]
}
Adding Normalize.css
In my _document.js I added the normalize
import Document, { Head, Main, NextScript } from 'next/document';
import normalize from 'normalize.css/normalize.css';
import { extractCritical } from 'emotion-server';
export default class MyDocument extends Document {
static getInitialProps({ renderPage }) {
const page = renderPage();
const styles = extractCritical(page.html);
return { ...page, ...styles };
}
constructor(props) {
super(props);
const { __NEXT_DATA__, ids } = props;
if (ids) {
__NEXT_DATA__.ids = ids;
}
}
render() {
return (
<html>
<Head>
<title>SSR</title>
<style jsx global>{normalize}</style>
<style dangerouslySetInnerHTML={{ __html: this.props.css }} />
</Head>
<body>
<Main />
<NextScript />
</body>
</html>
);
}
}
Same as shown here
Addin my css modules with Emotion
import React, { Component } from 'react';
import Breadcrumb from 'components/Breadcrumb';
import Link from 'next/link';
import styled, { hydrate, keyframes, css, injectGlobal } from 'react-emotion';
// Adds server generated styles to emotion cache.
// '__NEXT_DATA__.ids' is set in '_document.js'
if (typeof window !== 'undefined') {
hydrate(window.__NEXT_DATA__.ids);
}
const basicStyles = css`
background-color: white;
color: cornflowerblue;
margin: 3rem 0;
padding: 1rem 0.5rem;
`
const Basic = styled.div`
${basicStyles};
`
export default class extends Component {
render() {
return (
<Basic>
<p>Basic style rendered by emotion</p>
</Basic>);
}
}
Same as shown here
Problem
Error: StyleSheet: insertRule accepts only strings.
at invariant (/home/riderman/WebstormProjects/tmp/node_modules/styled-jsx/dist/lib/stylesheet.js:274:11)
at StyleSheet.insertRule (/home/riderman/WebstormProjects/tmp/node_modules/styled-jsx/dist/lib/stylesheet.js:125:7)
at /home/riderman/WebstormProjects/tmp/node_modules/styled-jsx/dist/stylesheet-registry.js:88:29
at Array.map (native)
at StyleSheetRegistry.add (/home/riderman/WebstormProjects/tmp/node_modules/styled-jsx/dist/stylesheet-registry.js:87:27)
at JSXStyle.componentWillMount (/home/riderman/WebstormProjects/tmp/node_modules/styled-jsx/dist/style.js:58:26)
at resolve (/home/riderman/WebstormProjects/tmp/node_modules/react-dom/cjs/react-dom-server.node.development.js:2616:12)
at ReactDOMServerRenderer.render (/home/riderman/WebstormProjects/tmp/node_modules/react-dom/cjs/react-dom-server.node.development.js:2746:22)
at ReactDOMServerRenderer.read (/home/riderman/WebstormProjects/tmp/node_modules/react-dom/cjs/react-dom-server.node.development.js:2722:19)
at renderToStaticMarkup (/home/riderman/WebstormProjects/tmp/node_modules/react-dom/cjs/react-dom-server.node.development.js:2991:25)
Added
Check source code here
https://gitlab.com/problems/test-emotion-plus-global-nextjs
Looks like there's an issue for this over on Zeit's styled-jsx page: https://github.com/zeit/styled-jsx/issues/298
According to this issue it is either external styles or that you need to add the css tag to your template literals.
Looking at your code you are using the css tag and don't see any externals styles that would be causing this. If you don't get a definite answer I'd say to follow up on issue 298 with Zeit. HTH, cheers!
Edit
Get rid of the jsx styles in there and just add normalize to your global template string:
injectGlobal`
${normalize}
html, body {
padding: 3rem 1rem;
margin: 0;
background: papayawhip;
min-height: 100%;
font-family: Helvetica, Arial, sans-serif;
font-size: 24px;
}
`;
I have an additional answer to this that works well in TypeScript and NextJS 9. It also keeps your import directly based on your node_modules.
Import raw-loader for the module:
yarn add raw-loader
In a global.d.ts, define a hook for raw-loader
declare module '!!raw-loader!*' {
const contents: string;
export = contents;
}
In a component called Meta I have inside my _document.tsx ( _app.tsx would be fine too, but _document ensures SSR), I have this
import normalizeCss from '!!raw-loader!normalize.css';
const Meta = () => (
<div>
<Global
styles={css`
${normalizeCss}
body {
// ...
}
`}
></Global>
</div>
);
export default Meta;
Related
I'v try to make a SlectBox with react native.
To make that dropdown menu can be floated, I gava an absolute position to wrapper View, then make a clickable component with TouchableOpacity inside that View. It works well on Ios and Web, but not on android. There were many solutions on google like reordering component or give a zIndex to WrapperView or use Pressable etc.., but I could not find proper solution for my case.
Belows are my whole code. and You can test this code here.
Thank you in advance.
import {
Animated,
Easing,
Platform,
StyleProp,
TextStyle,
TouchableOpacity,
View,
ViewStyle,
} from 'react-native';
import {DoobooTheme, light, useTheme} from '../theme';
import React, {FC, ReactElement, useEffect, useRef, useState} from 'react';
import {Icon} from '../Icon';
import {Typography} from '../Typography';
import styled from '#emotion/native';
import {withTheme} from '#emotion/react';
const Title = styled.View`
width: 200px;
height: 30px;
border-width: 1px;
flex-direction: row;
justify-content: center;
align-items: center;
`;
const Item = styled.View`
height: 30px;
width: 200px;
border-bottom-width: 1px;
border-left-width: 1px;
border-right-width: 1px;
justify-content: center;
align-items: center;
`;
type Styles = {
titleContainer?: StyleProp<ViewStyle>;
titleText?: StyleProp<TextStyle>;
rightElementContainer?: StyleProp<ViewStyle>;
itemContainer?: StyleProp<ViewStyle>;
itemText?: StyleProp<TextStyle>;
};
interface ItemCompProps {
value: string;
order: number;
styles?: Styles;
setIsOpened: (value: boolean) => void;
itemActiveOpacity: number;
onPress?: (i: number) => void;
}
const ItemComp: FC<ItemCompProps> = ({
value,
order,
styles,
setIsOpened,
itemActiveOpacity,
onPress,
}) => {
const {theme} = useTheme();
const handlePress = (): void => {
onPress?.(order);
setIsOpened(false);
};
return (
<TouchableOpacity onPress={handlePress} activeOpacity={itemActiveOpacity}>
<Item
style={[
{
borderColor: theme.primary,
backgroundColor: theme.textContrast,
},
styles?.itemContainer,
]}>
<Typography.Body2 style={styles?.itemText}>{value}</Typography.Body2>
</Item>
</TouchableOpacity>
);
};
interface Props {
data: string[];
onPress?: (i: number) => void;
selectedIndex?: number;
theme?: DoobooTheme;
style?: StyleProp<ViewStyle>;
styles?: Styles;
rotateDuration?: number;
titleActiveOpacity?: number;
itemActiveOpacity?: number;
isRotate?: boolean;
rightElement?: ReactElement | null;
}
const Component: FC<Props> = ({
data,
onPress,
selectedIndex = 0,
style,
styles,
rotateDuration = 200,
titleActiveOpacity = 1,
itemActiveOpacity = 1,
isRotate: shouldRotate = true,
rightElement = <Icon name="chevron-down-light" />,
}) => {
const {theme} = useTheme();
const [isOpened, setIsOpened] = useState(false);
const rotateAnimValue = useRef(new Animated.Value(0)).current;
useEffect(() => {
const toValue = isOpened ? 1 : 0;
if (!shouldRotate) rotateAnimValue.setValue(toValue);
Animated.timing(rotateAnimValue, {
toValue,
duration: rotateDuration,
easing: Easing.linear,
useNativeDriver: Platform.OS !== 'web' ? true : false,
}).start();
}, [isOpened, rotateAnimValue, rotateDuration, shouldRotate]);
return (
<View style={[style]}>
<TouchableOpacity
onPress={() => setIsOpened((prev) => !prev)}
activeOpacity={titleActiveOpacity}>
<Title
style={[
{
borderColor: theme.primary,
backgroundColor: theme.textContrast,
},
styles?.titleContainer,
]}>
<Typography.Body2 style={styles?.titleText} testID="selected-value">
{data[selectedIndex]}
</Typography.Body2>
{rightElement ? (
<Animated.View
style={[
{
position: 'absolute',
right: 10,
transform: [
{
rotate: rotateAnimValue.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '180deg'],
}),
},
],
},
styles?.rightElementContainer,
]}>
{rightElement}
</Animated.View>
) : null}
</Title>
</TouchableOpacity>
<View>
<View style={{position: 'absolute'}}>
{isOpened &&
data.map((datum, key) => (
<ItemComp
key={key}
order={key}
value={datum}
styles={styles}
setIsOpened={setIsOpened}
onPress={onPress}
itemActiveOpacity={itemActiveOpacity}
/>
))}
</View>
</View>
</View>
);
};
Component.defaultProps = {theme: light};
export const SelectBox = withTheme(Component);
My solution
I resolved this problem with react-native-gesture-handler. :)
Install react-native-gesture-handler, then using TouchableOpacity from that.
Try to render out the component you want to be on top of stack below the component you want to bring below it
How I solve this issue:
I was implementing custom dynamic toaster using context to use it globally and wasn't able to access touchable opacity due to absolute positioning but i realized i was rendering my Toaster component before my main Stack Navigator which was bringing my toaster below navigation layer so i rendered it after stack navigator and it worked
BEFORE:
<ToasterContext.Provider value={{toasts:this.state.toastState, showToaster: this.showToaster,closeToaster: this.closeToaster }}>
<Provider store={store.store}>
<PersistGate loading={null} persistor={store.persistor}>
{toastState.length > 0
&& toastState.length <= 3
&& toastState.map((t, i) => {
return(
<DynamicToast i={i} key={i} t={t} position={"bottom"} duration={4000} closeToaster={this.closeToaster} />
)
}
)}
<NetworkConnectionModal/>
<AppContainer /> // I was rendering before my AppContainer
</PersistGate>
</Provider>
</ToasterContext.Provider>
AFTER: (Resolved my problem)
<ToasterContext.Provider value={{toasts:this.state.toastState, showToaster: this.showToaster,closeToaster: this.closeToaster }}>
<Provider store={store.store}>
<PersistGate loading={null} persistor={store.persistor}>
<NetworkConnectionModal/>
<AppContainer />
{toastState.length > 0
&& toastState.length <= 3
&& toastState.map((t, i) => {
return(
<DynamicToast i={i} key={i} t={t} position={"bottom"} duration={4000} closeToaster={this.closeToaster} />
)
}
)}
</PersistGate>
</Provider>
</ToasterContext.Provider>
Most of this is because the element is hidden behind other elements after absolute positioning.
Just like the picture in PPT is set to the bottom, but in some cases in HTML, you can still see your element. In this case, we need to set this element to the top.
Open the developer tool, select your element, add a Z-index to the element style of this element, and increase from 2 to see which value can just select your element. After confirmation, add a style to the project file.
z-index: 2,//add this to your component style
I am building a prototype video web application using latest opentok client API 2.18.0 and the Ember.js framework.
I have a simple Ember.js page, controller and css example which connects OK to Vonage video API but the page video div DOM targetElement ("publisher") is not replaced.
All I see is the published video in a new DOM element appended to the HTML body.
Question, why is the targetElement not replaced?
Changing the publisher targetElement to an invalid name does not throw an error and behaves exactly the same.
OT.initPublisher('publisherINVALID'
My page
{{global/site-header}}
{{#global/app-container}}
<div class="Container">
{{!-- TODO opentok is not putting video here? --}}
<div id="videos" class="VideoParticipant">
<div id="subscriber" class="VideoParticipant-subscriber"></div>
<div id="publisher" class="VideoParticipant-publisher"></div>
</div>
{{forms/buttons/button-action
class="Button--block"
text='START'
onClick=(action 'start')
}}
</div>
{{/global/app-container}}
My controller
import Ember from 'ember';
import OT from '#opentok/client';
const {
Controller,
Object: EmberObject,
} = Ember;
// TODO get session, token from server
const apiKey = "REMOVED";
const sessionId = "REMOVED";
const token = "REMOVED";
export default Controller.extend({
init() {
this._super(...arguments);
this.initializeSession();
},
initializeSession() {
var session = OT.initSession(apiKey, sessionId);
this.session = session;
// Subscribe to a newly created stream
session.on('streamCreated', function(event) {
session.subscribe(event.stream, 'subscriber', {
insertMode: 'replace'
}, function(error) {
if (error) {
console.log('There was an error subscribing: ', error.name, error.message);
return;
}
});
});
// Create a publisher
var publisher = OT.initPublisher('publisher', {
insertMode: 'replace'
}, function(error) {
if (error) {
console.log('There was an error initializing publisher: ', error.name, error.message);
return;
}
});
// Connect to the session
session.connect(token, function(error) {
// If the connection is successful, initialize a publisher and publish to the session
if (error) {
console.log('There was an error connecting to session: ', error.name, error.message);
} else {
session.publish(publisher, function(error) {
if (error) {
console.log('There was an error publishing: ', error.name, error.message);
}
});
console.log("INIT VIDEO SESSION PUBLISHED");
}
});
},
actions: {
start() {
console.log("TODO CH START");
},
cancel() {
this.send('no');
},
},
});
My CSS
.VideoParticipant {
position: relative;
width: 100%;
height: 100%;
margin-left: auto;
margin-right: auto;
}
.VideoParticipant-subscriber {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 10;
}
.VideoParticipant-publisher {
position: relative;
width: 360px;
height: 240px;
margin-bottom: 10px;
margin-left: 10px;
z-index: 100;
border: 3px solid white;
border-radius: 3px;
}
It may be that Ember has not yet rendered the HTML in your template when OT.initPublisher is called.
To check to see if this is the issue, you could add a debugger immediately before the OT.initPublisher line, and inspect the DOM.
If that is the issue, you could work around it by scheduling your code run after rendering is complete. You could do this by replacing the call to this.initializeSession in the init method of the controller, with schedule('afterRender', this, this.initializeSession). Import schedule using import { schedule } from '#ember/runloop';
Alternatively, if you are on a recent version of Ember (3.12 or higher), you can look into using the {{did-insert ...}} modifier to invoke the initialization instead of scheduling it on the runloop.
With css-loader
{
test: /\.s?css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader',
query: {
modules: true,
localIdentName: '[name]-[local]-[hash:base64:8]'
}
},
{ loader: 'sass-loader'}
]
}
configured that way the css-loader seems to not find css rules under class names. The css rules listed under div.profile doesn't get applied on the screen. The css-loader ver. 1.0.0 in my code runs with Node 10.x. Switching modules: false gets the desired styling to show.
The code is posted below.
main.js:
require('babel-runtime/regenerator');
require('babel-register');
require('webpack-hot-middleware/client?reload=true');
require('./index.html');
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack 4</title>
</head>
<body>
<div class="profile">
<img src="./images/400.jpg" alt="">
<h1>Hello Webpack 4</h1>
<div id="react-root"></div>
</div>
<script src="/main-bundle.js"></script>
</body>
</html>
app.js:
import React from 'react';
import ReactDOM from 'react-dom';
import Counter from './counter';
import { AppContainer } from 'react-hot-loader';
const render = (Component) => {
ReactDOM.render(
<AppContainer>
<Component />
</AppContainer>,
document.getElementById('react-root')
);
};
render(Counter);
if (module.hot) {
module.hot.accept('./counter', () => {
render(require('./counter'));
});
}
counter.js:
import React, { Component } from 'react';
import { hot } from 'react-hot-loader';
import { css } from 'emotion';
import styled from 'react-emotion';
import styles from './main.scss';
const Fancy = styled('h1')`
color: ${props => props.wild ? 'hotpink' : 'gold'}
`;
const red = '#f00';
const className = css`
color: ${red};
font-size: 3rem;
`;
class Counter extends Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.addCount = this.addCount.bind(this);
}
addCount() {
this.setState(() => ({ count: this.state.count + 1 }));
}
render() {
const isWild = this.state.count % 2 === 0;
return (
<div className={styles.counter}>
<h1 onClick={this.addCount} className={className}>Count: {this.state.count}</h1>
<Fancy wild={isWild}>react-emotion lib allows to hook styles to component names</Fancy>
</div>
);
}
}
export default hot(module)(Counter);
main.scss:
body {
background-color: #a1b2c3;
}
.profile {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
img {
border-radius: 50%;
box-shadow: 0 0 20px #000;
}
h1 {
font-family: 'source-code-pro', 'sans-serif';
font-weight: 400;
}
}
.counter {
border: 3px solid green;
}
The reason was the .profile class name in index.html is outside the counter.js scope. The css modules produce class names by the localIdentName pattern but the .profile class name was hard coded in index.html before css modules in counter.js came into play.
In counter.js
import styles from './main.scss';
console.log('styles:', styles);
outputs
styles: Object { profile: "main-profile-2P-yNf0J", counter: "main-counter-Pmp5YERO" }
How to get the main-profile-2P-yNf0J class name to index.html remains unclear for me.
How to create a print stylesheet which can override the dynamic styles created by css modules ?
Using CSS modules, classnames render with unique names like so :
<button class="buttons_style_primary-button__3T" type="submit"> Submit</button>
In my print stylesheet I have the following, which has no effect :
#media print {
button {
display: none;
}
}
I can get it to work by adding !important to the button style, but I will have many print styles and I don't want to do this for each style attribute. Is there an alternative ?
I'm also using React if there happens to be a React specific approach here.
Wound up doing the following :
Use !important for globals that need to override local values:
/* app.css */
#media print {
#page {
margin: 1cm;
}
* {
color: #000 !important;
}
}
Put component specific overrides into a style.css for each component:
/* style.css */
.my-class {
composes: rounded-corners from 'shared/ui.css';
margin: 0 0 60px 0;
background-color: white;
}
#media print {
.my-class {
page-break-inside: avoid;
font-size: 10px;
}
}
/* my-component.jsx */
import style from './style.css';
const MyComponent = () => {
return (
<div className={style.myClass}>
....
</Link>
);
};
There's also a third option which I haven't really tried.
You should be able to apply both the top-level override classname with your local classname using the classNames library:
import app from 'app.css';
import styles from './style.ss'
const MyComponent = () => {
return (
<div className={classNames(style.local, app.global)}>
....
</Link>
);
};
( this third option is just off the top of my head, I don't know if it will work )
Instead of this:
#media print {
.button {
display: none;
}
}
Try this:
.button{
#media print {
display: none;
}
}
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()