Calling a Vue mixin directly with a constant? - javascript

I have a footer component with three links. When a user clicks a link, besides taking the user to a new page, I am trying to use a mixin to track the click event. When I set a breakpoint in chrome devtools, it appears that this implementation is not working. I imported my constants file, and the mixin.
footer, one link for brevity
<template>
<footer>
<div class="container">
<div class="row">
<div class="col align-center">
<a
href="/"
target="_blank"
class="btn"
name="item"
#click="logButtonClick(ANALYTICS.ITEM)">{{ $t('footer.item') }}</a>
</div>
</div>
</div>
</footer>
</template>
<script>
import analytics from '#/mixins/analytics'
import { ANALYTICS } from "#/constants"
export default {
name: 'PageFooter',
mixins: [analytics]
}
</script>
mixin
methods: {
logButtonClick (buttonType) { // breakpoint here, get nothing
this.$analytics.track({
identifier: `Consumer ${this.$options.name} - ${buttonType} Button`
})
}
}
Am I missing something? Should this implementation work or should I have a method such as:
methods: {
selectLink(str) {
if (str === item) {
this.logButtonClick(ANALYTICS.ITEM)
}
}
}

The original error I received was
"Property or method ANALYTICS not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class based components, by initializing the property."
and
"Uncaught TypeError: Cannot read property ITEM of undefined at click event...."
Essentially this means I did not define ANALYTICS either in PageFooter (because this is a dumb component, I did not want to add a data object to it, I wanted to keep it strictly presentational) or on the vue instance at a root level. Since ANALYTICS is undefined, ITEM then throws another error because it can not be a property of undefined.
This is my solution, I used a switch case in the and in the template tag added #click="selectLink('asd')"
methods: {
selectLink (str) {
switch (true) {
case str === 'asd':
this.logButtonClick(ANALYTICS.ITEM)
break
case str === 'efg':
this.logButtonClick(ANALYTICS.ITEM2)
break
case str === 'hij':
this.logButtonClick(ANALYTICS.ITEM3)
break
}
}
}
and the unit test:
it('[positive] should track analytics if `asd` is passed to selectLink()', () => {
const str = 'asd'
const mockFn = jest.fn()
jest.spyOn(wrapper.vm, 'logButtonClick')
wrapper.find('a').trigger('click')
mockFn(str)
expect(wrapper.vm.logButtonClick).toHaveBeenCalledWith(ANALYTICS.COOKIE_CONSENT)
})
https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties
Moral of the story, question things when senior engineers tell you to do something funky in a code review.

Related

Using Javascript to create html custom Tag

class Headers extends React.Component {
render() {
const selected = this.props.selectedPane;
const headers = this.props.panes.map((pane, index) => {
const title = pane.title;
const klass = index === selected ? 'active' : '';
return (
<li
key={index}
className={klass}
onClick={() => this.props.onTabChosen(index)}>
{title}{' '}
</li>
);
});
return (
<ul className='tab-header'>
{headers}
</ul>
);
}
}
export default class Tabs extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedPane: 0
};
this.selectTab = this.selectTab.bind(this);
}
selectTab(num) {
this.setState({selectedPane: num});
}
render() {
const pane = this.props.panes[this.state.selectedPane];
return (
<div>
<h1>Tabs</h1>
<div className='tabs'>
<Headers
selectedPane={this.state.selectedPane}
//onTabChosen={this.selectTab}
panes={this.props.panes}>
</Headers>
<div className='tab-content'>
<article>
hellooooo
{pane.content}
</article>
</div>
</div>
</div>
);
}
}
I'm currently creating a 3 tab section where if you click on a tab, it gives you a new pane.
When looking at the render function I see a custom tag called Headers.
I know it coming from the Headers class at the beginning, but how does that format work? Is that a custom tag we building?
Also when looking at its properties such as onTabChosen, when it is deleted in the render method (for learning purposes) and I click on a selected tab, an error comes up saying
"_this.props.onTabChosen is not a function".
this.props.onTabChosen(index).. was written in the Headers class but not as a function correct?
I guess because I am also confused on how this.props.onTabChosen(index) works since onTabChosen was never declared anywhere, just input after props.
When looking at the render function I see a custom tag called "Headers".
That is not a custom tag. That is a React Component.
I know it coming from the Headers class at the beginning, but how does that format work?
Headers is either a function or a class (i.e. a constructor function).
The function will be called and the first argument passed to it will be an object with properties and values that match the props on the JSX element.
If you're going to use React then read a tutorial, this is very introductory level stuff for the framework.
It is covered very early on in both the MDN tutorial and the official React tutorial.
I guess because I am also confused on how this.props.onTabChosen(index) works since onTabChosen was never declared anywhere, just input after props.
It was declared, just not in the piece of code you shared.

"el.closest is not a function at getScrollTarget", using Quasar Framework (Vue)

My modal consists of cards which works fine. Now I want to have the 'active' card at the top of the viewport when the modal opens. I am trying to do this using getScrollTarget and setScrollPosition but I get the following error:
Uncaught TypeError: el.closest is not a function at getScrollTarget (quasar.esm.js?8bfb:1384)
at VueComponent.open (ModalTest.vue?4a7f:120)
at VueComponent.boundFn [as open] (vue.runtime.esm.js?ff9b:165)
at VueComponent.openModal (Maplayout.vue?1988:60)
at VueComponent.boundFn [as openModal] (vue.runtime.esm.js?ff9b:165)
at Vue$3.eval (Maplayout.vue?1988:47)
at Vue$3.Vue.$emit (vue.runtime.esm.js?ff9b:2202)
at Object.$emit (quasar.esm.js?8bfb:198)
at HTMLElement.emitEventFunction (util.js?93ee:587)
The modal component looks like this:
<template>
<q-modal ref="myModal" maximized>
<q-modal-layout class="scroll">
<div class="layout-padding">
<q-card inline
center
style="min-width: 90vw"
v-for="test in tests"
:key="tests.id"
>
<q-card-media>
<img :src="test.getAttribute('image')">
</q-card-media>
<q-card-main>
<div>{{ test.getAttribute('myAttribute') }}</div>
</q-card-main>
</q-card>
</div>
</q-modal-layout>
</q-modal>
</template>
<script>
import {
QCard,
QCardMain,
QCardMedia,
QModal,
QModalLayout,
scroll
} from 'quasar'
const {getScrollTarget, setScrollPosition} = scroll
export default {
name: 'myModal',
components: {
QCard,
QCardMain,
QCardMedia,
QModal,
QModalLayout,
scroll
},
computed: {
...myState({
tests: state => state.mymodule.tests
})
},
methods: {
open () {
this.$refs.myModal.open()
const element = document.getElementById('feature-listing').getElementsByClassName('item-image active')
console.log('element: ', element)
setScrollPosition(getScrollTarget(element), 0, 200)
}
}
}
</script>
The output of the console.log gives following about the element that is active in the console:
[div#98729.item-image.js-fade.my-quick-fade-in.active, 98729: undefined]
I do not quite understand the 'undefined' part... (the length of the array is 1..., so maybe not relevant or something...?!), as the div can be opened in the console and looks good to me.
I am trying to follow this in the docs. I also looked into the code for the polyfill for 'closest' here.
Cause of error is that no valid div is referenced. When I use element[0] it starts working.
Furthermore scroll methods in the modal will only give value after modal properly opened, so need to use the #open event. See this for details.

The content not shown properly in function callback in Vue.js

I've got two problems here. The first is that I can't get the star rendered properly. I can do it if I change the value in the data() function but if I want to do it in a function callback way, it doesn't work (see comments below). What's going wrong here? Does it have something to do with Vue's lifecycle?
The second one is that I want to submit the star-rate and the content of the textarea and when I refresh the page, the content should be rendered on the page and replace the <textarea></textarea> what can I do?
I want to make a JSFiddle here but I don't know how to make it in Vue's single-file component, really appreciate your help.
<div class="order-comment">
<ul class="list-wrap">
<li>
<span class="comment-label">rateA</span>
<star-rating :data="dimensionA"></star-rating>
</li>
</ul>
<div>
<h4 class="title">comment</h4>
<textarea class="content" v-model="content">
</textarea>
</div>
<mt-button type="primary" class="mt-button">submit</mt-button>
</div>
</template>
<script>
import starRating from 'components/starRating'
import dataService from 'services/dataService'
export default {
data () {
return {
dimensionA: '' //if I changed the value here the star rendered just fine.
}
},
components: {
starRating
},
methods: {
getComment (id) {
return dataService.getOrderCommentList(id).then(data => {
this.dimensionA = 1
})
}
},
created () {
this.getComment(1) // not working
}
}
</script>
What it seems is scope of this is not correct in your getComment method, you need changes like following:
methods: {
getComment (id) {
var self = this;
dataService.getOrderCommentList(id).then(data => {
self.dimensionA = 1
})
}
},
As you want to replace the <textarea> and render the content if present, you can use v-if for this, if content if available- show content else show <textarea>
<div>
<h4 class="title">comment</h4>
<span v-if="content> {{content}} </span>
<textarea v-else class="content" v-model="content">
</textarea>
</div>
See working fiddle here.
one more problem I have observed in your code is you are using dynamic props, but you have assigned the prop initially to the data variable value in star-rating component, but you are not checking future changes in the prop. One way to solve this, assuming you have some other usage of value variable is putting following watch:
watch:{
data: function(newVal){
this.value = newVal
}
}
see updated fiddle.

How to combine JSX component with dangerouslySetInnerHTML

I'm displaying text that was stored in the database. The data is coming from firebase as a string (with newline breaks included). To make it display as HTML, I originally did the following:
<p className="term-definition"
dangerouslySetInnerHTML={{__html: (definition.definition) ? definition.definition.replace(/(?:\r\n|\r|\n)/g, '<br />') : ''}}></p>
This worked great. However there's one additional feature. Users can type [word] and that word will become linked. In order to accomplish this, I created the following function:
parseDefinitionText(text){
text = text.replace(/(?:\r\n|\r|\n)/g, '<br />');
text = text.replace(/\[([A-Za-z0-9'\-\s]+)\]/, function(match, word){
// Convert it to a permalink
return (<Link to={'/terms/' + this.permalink(word) + '/1'}>{word}</Link>);
}.bind(this));
return text;
},
I left out the this.permalink method as it's not relevant. As you can see, I'm attempting to return a <Link> component that was imported from react-router.However since it's raw HTML, dangerouslySetInnerHTML no longer works properly.
So I'm kind of stuck at this point. What can I do to both format the inner text and also create a link?
You could split the text into an array of Links + strings like so:
import {Link} from 'react-router';
const paragraphWithLinks = ({markdown}) => {
const linkRegex = /\[([\w\s-']+)\]/g;
const children = _.chain(
markdown.split(linkRegex) // get the text between links
).zip(
markdown.match(linkRegex).map( // get the links
word => <Link to={`/terms/${permalink(word)}/1`}>{word}</Link> // and convert them
)
).flatten().thru( // merge them
v => v.slice(0, -1) // remove the last element (undefined b/c arrays are different sizes)
).value();
return <p className='term-definition'>{children}</p>;
};
The best thing about this approach is removing the need to use dangerouslySetInnerHTML. Using it is generally an extremely bad idea as you're potentially creating an XSS vulnerability. That may enable hackers to, for example, steal login credentials from your users.
In most cases you do not need to use dangerouslySetHTML. The obvious exception is for integration w/ a 3rd party library, which should still be considered carefully.
I ran into a similar situation, however the accepted solution wasn't a viable option for me.
I got this working with react-dom in a fairly crude way. I set the component up to listen for click events and if the click had the class of react-router-link. When this happened, if the item has a data-url property set it uses browserHistory.push. I'm currently using an isomorphic app, and these click events don't make sense for the server generation, so I only set these events conditionally.
Here's the code I used:
import React from 'react';
import _ from 'lodash';
import { browserHistory } from 'react-router'
export default class PostBody extends React.Component {
componentDidMount() {
if(! global.__SERVER__) {
this.listener = this.handleClick.bind(this);
window.addEventListener('click', this.listener);
}
}
componentDidUnmount() {
if(! global.__SERVER__) {
window.removeEventListener("scroll", this.listener);
}
}
handleClick(e) {
if(_.includes(e.target.classList, "react-router-link")) {
window.removeEventListener("click", this.listener);
browserHistory.push(e.target.getAttribute("data-url"));
}
}
render() {
function createMarkup(html) { return {__html: html}; };
return (
<div className="col-xs-10 col-xs-offset-1 col-md-6 col-md-offset-3 col-lg-8 col-lg-offset-2 post-body">
<div dangerouslySetInnerHTML={createMarkup(this.props.postBody)} />
</div>
);
}
}
Hope this helps out!

React Testing: Event handlers in React Shallow Rendering unit tests

Background
I am trying to learn how to use the React Shallow Rendering TestUtil and had the tests passing until I added an onClick event handler to both; It seems that there must be some difference with the Accordion.toggle function I am trying to use in Accordion.test.js vs this.toggle in Accordian.js...but I can't figure it out.
Question
How can I get the two highlighted tests in Accordian.test.js to pass?
Steps to reproduce
Clone https://github.com/trevordmiller/shallow-rendering-testing-playground
npm install
npm run dev - see that component is working when you click "Lorem Ipsum"
npm run test:watch - see that tests are failing
There are a number of issues preventing your tests from passing.
Looking at the test "should be inactive by default":
Accordion.toggle in your test is a property of the Accordion class, and this.toggle in your code is a property of a instance of the Accordion class - so in this case you are comparing two different things. To access the 'instance' method in your test you could replace Accordion.toggle with Accordion.prototype.toggle. Which would work if it were not for this.toggle = this.toggle.bind(this); in your constructor. Which leads us to the second point.
When you call .bind() on a function it creates a new function at runtime - so you can't compare it to the original Accordion.prototype.toggle. The only way to work around this is to pull the "bound" function out of the result from render:
let toggle = result.props.children[0].props.onClick;
assert.deepEqual(result.props.children, [
<a onClick={toggle}>This is a summary</a>,
<p style={{display: 'none'}}>This is some details</p>
]);
As for your second failing test "should become active when clicked":
You try calling result.props.onClick() which does not exist. You meant to call result.props.children[0].props.onClick();
There is a bug in React that requires a global "document" variable to be declared when calling setState with shallow rendering - how to work around this in every circumstance is beyond the scope of this question, but a quick work around to get your tests passing is to add global.document = {}; right before you call the onClick method. In other words where your original test had:
result.props.onClick();
Should now say:
global.document = {};
result.props.children[0].props.onClick();
See the section "Fixing Broken setState()" on this page and this react issue.
Marcin Grzywaczewski wrote a great article with a workaround for testing a click handler that works with shallow rendering.
Given a nested element with an onClick prop and a handler with context bound to the component:
render() {
return (
<div>
<a className="link" href="#" onClick={this.handleClick}>
{this.state.linkText}
</a>
<div>extra child to make props.children an array</div>
</div>
);
}
handleClick(e) {
e.preventDefault();
this.setState({ linkText: 'clicked' });
}
You can manually invoke the function value of the onClick prop, stubbing in the event object:
it('updates link text on click', () => {
let tree, link, linkText;
const renderer = TestUtils.createRenderer();
renderer.render(<MyComponent />);
tree = renderer.getRenderOutput();
link = tree.props.children[0];
linkText = link.props.children;
// initial state set in constructor
expect(linkText).to.equal('Click Me');
// manually invoke onClick handler via props
link.props.onClick({ preventDefault: () => {} });
tree = renderer.getRenderOutput();
link = tree.props.children[0];
linkText = link.props.children;
expect(linkText).to.equal('Clicked');
});
For testing user events like onClick you would have to use TestUtils.Simulate.click. Sadly:
Right now it is not possible to use ReactTestUtils.Simulate with Shallow rendering and i think the issue to follow should be: https://github.com/facebook/react/issues/1445
I have successfully tested my click in my stateless component. Here is how:
My component:
import './ButtonIcon.scss';
import React from 'react';
import classnames from 'classnames';
const ButtonIcon = props => {
const {icon, onClick, color, text, showText} = props,
buttonIconContainerClass = classnames('button-icon-container', {
active: showText
});
return (
<div
className={buttonIconContainerClass}
onClick={onClick}
style={{borderColor: color}}>
<div className={`icon-container ${icon}`}></div>
<div
className="text-container"
style={{display: showText ? '' : 'none'}}>{text}</div>
</div>
);
}
ButtonIcon.propTypes = {
icon: React.PropTypes.string.isRequired,
onClick: React.PropTypes.func.isRequired,
color: React.PropTypes.string,
text: React.PropTypes.string,
showText: React.PropTypes.bool
}
export default ButtonIcon;
My test:
it('should call onClick prop when clicked', () => {
const iconMock = 'test',
clickSpy = jasmine.createSpy(),
wrapper = ReactTestUtils.renderIntoDocument(<div><ButtonIcon icon={iconMock} onClick={clickSpy} /></div>);
const component = findDOMNode(wrapper).children[0];
ReactTestUtils.Simulate.click(component);
expect(clickSpy).toHaveBeenCalled();
expect(component).toBeDefined();
});
The important thing is to wrap the component:
<div><ButtonIcon icon={iconMock} onClick={clickSpy} /></div>
Hope it help!

Categories