GraphQL query callbacks for Gatsby.js - javascript

In the Contentful CMS, I have two different content-types: BigCaseStudy and BigCaseStudySection. To get this content to appear in my Gatsby 2.x site, my thinking was:
Do query 1, which gets all the BigCaseStudy fields I want to display, and also contains the content's ID field as metadata.
Take that ID from query 1, match to a Contentful reference field (which contains an ID) in query 2
Do query 2, return all matching BigCaseStudySection fields
The end goal would be to display the original BigCaseStudy with all of the BigCaseStudySection (usually numbering between 3-5 of them). You can look at my queries to see the fields, there are bunch.
I think some combination of GraphQL variables and queries would get me there (maybe a mutation)? It's not clear and I haven't seen any complex examples of querying one set of stuff and then using the response to make another call, like chained promises or async/await in JS.
Any ideas on the right construction?
bigcasestudy.js component with GraphQL queries:
import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import { graphql } from 'gatsby'
import Img from 'gatsby-image'
import Layout from '../Layout/layout'
/**
* Hero Section
*/
const HeroContainer = styled.header`
align-items: center;
background-image: url(${ props => props.bgImgSrc });
background-position: center center;
background-size: cover;
display: flex;
flex-direction: column;
justify-content: center;
height: calc(100vh - 128px);
`
const HeroTitle = styled.h1`
color: #fff;
font-size: 70px;
font-weight: 700;
letter-spacing: 2px;
margin-bottom: 15px;
`
const HeroSubtitle = styled.h2`
color: #fff;
font-size: 24px;
font-weight: 300;
letter-spacing: 5px;
text-transform: uppercase;
`
/**
* Intro Section
*/
const IntroBG = styled.section`
background-color: ${ props => props.theme.lightGray };
padding: 50px 0;
`
const IntroContainer = styled.div`
padding: 25px;
margin: 0 auto;
max-width: ${ props => props.theme.sm };
#media (min-width: ${ props => props.theme.sm }) {
padding: 50px 0;
}
`
const IntroTitle = styled.h2`
font-size: 50px;
font-weight: 700;
letter-spacing: 2px;
margin-bottom: 45px;
text-align: center;
`
const IntroText = styled.p`
font-size: 22px;
line-spacing: 4;
text-align: center;
`
const IntroButton = styled.a`
background-color: #fff;
color: ${ props => props.theme.darkGray };
border: 1px solid ${ props => props.theme.mediumGray };
border-radius: 25px;
display: block;
font-size: 16px;
letter-spacing: 5px;
margin: 30px auto;
padding: 15px 45px;
text-align: center;
text-decoration: none;
text-transform: uppercase;
width: 300px;
`
// BigCaseStudy Component
class BigCaseStudy extends React.Component {
render() {
// Setup destructured references to each Contentful object passed in through the GraphQL call
const { caseStudyTitle } = this.props.data.contentfulBigCaseStudy
const { caseStudySubtitle } = this.props.data.contentfulBigCaseStudy
const { caseStudyIntroTitle } = this.props.data.contentfulBigCaseStudy
const { caseStudyIntro } = this.props.data.contentfulBigCaseStudy.caseStudyIntro
const { caseStudyLink } = this.props.data.contentfulBigCaseStudy
console.log(this)
return (
<Layout>
<HeroContainer
bgImgSrc={ this.props.data.contentfulBigCaseStudy.caseStudyHero.fixed.src }>
<HeroTitle>{ caseStudyTitle }</HeroTitle>
<HeroSubtitle>{ caseStudySubtitle }</HeroSubtitle>
</HeroContainer>
<IntroBG>
<IntroContainer>
<IntroTitle>{ caseStudyIntroTitle }</IntroTitle>
<IntroText>{ caseStudyIntro }</IntroText>
</IntroContainer>
<IntroButton href={ caseStudyLink } target="_blank" rel="noopener noreferrer">
Visit the site >
</IntroButton>
</IntroBG>
</Layout>
)
}
}
// Confirm data coming out of contentful call is an object
BigCaseStudy.propTypes = {
data: PropTypes.object.isRequired
}
// Export component
export default BigCaseStudy
// Do call for the page data
// This needs to mirror how you've set up the dynamic createPage function data in gatsby-node.js
export const BigCaseStudyQuery = graphql`
query BigCaseStudyQuery {
contentfulBigCaseStudy {
id
caseStudyTitle
caseStudySubtitle
caseStudyIntroTitle
caseStudyIntro {
caseStudyIntro
}
caseStudyLink
caseStudyHero {
fixed {
width
height
src
srcSet
}
}
},
contentfulBigCaseStudySection (id: $postId) {
title
order
images {
fixed {
width
height
src
srcSet
}
}
bigCaseStudyReference {
id
}
body {
body
}
stats {
stat1 {
word
number
}
stat2 {
word
number
}
stat3 {
word
number
}
stat4 {
word
number
}
}
id
}
}
`
gatsby-node.js file:
/**
* Implement Gatsby's Node APIs in this file.
*
* ######################################################
* BIG CASE STUDY BACKEND CODE
* ######################################################
*
* We are using the .createPages part of the Gatsby Node API: https://next.gatsbyjs.org/docs/node-apis/#createPages
* What this does is dynamically create pages (surprise) based on the data you feed into it
*
* Feed the contentful API call into the promise
* Here I'm calling BigCaseStudy, which is a custom content type set up in contentful
* This is briefly explained over here: https://www.gatsbyjs.org/packages/gatsby-source-contentful/
*
* Also, note the caseStudyIntro field is long text `markdown`
* Gatsby returns the long text field as an object
* Calling it's name inside of the object returns the HTML
* Read more here: https://github.com/gatsbyjs/gatsby/issues/3205
*/
// Set Gatsby path up to be used by .createPages
const path = require('path')
// Using Node's module export, Gatsby adds in a createPages factory
exports.createPages = ({ graphql, actions }) => {
// We setup the createPage function takes the data from the actions object
const { createPage } = actions
// Setup a promise to build pages from contentful data model for bigCaseStudies
return new Promise((resolve, reject) => {
// Setup destination component for the data
const bigCaseStudyComponent = path.resolve('src/components/BigCaseStudy/bigcasestudy.js')
resolve(
graphql(`
{
allContentfulBigCaseStudy {
edges {
node {
id
caseStudySlug
caseStudyTitle
caseStudySubtitle
caseStudyIntroTitle
caseStudyIntro {
caseStudyIntro
}
caseStudyLink
caseStudyHero {
fixed {
width
height
src
srcSet
}
}
}
}
}
allContentfulBigCaseStudySection {
edges {
node {
title
order
images {
fixed {
width
height
src
srcSet
}
}
bigCaseStudyReference {
id
}
body {
body
}
stats {
stat1 {
word
number
}
stat2 {
word
number
}
stat3 {
word
number
}
stat4 {
word
number
}
}
id
}
}
}
}
`).then((result) => {
// Now we loop over however many caseStudies Contentful sent back
result.data.allContentfulBigCaseStudy.edges.forEach((caseStudy) => {
const caseStudySections = result.data.allContentfulBigCaseStudySection.edges
let caseStudySectionMatches = caseStudySections.filter(
caseStudySection => caseStudySection.bigCaseStudyReference.id === caseStudy.id
)
createPage ({
path: `/work/${caseStudy.node.caseStudySlug}`,
component: bigCaseStudyComponent,
context: {
id: caseStudy.node.id,
slug: caseStudy.node.caseStudySlug,
title: caseStudy.node.caseStudyTitle,
subtitle: caseStudy.node.caseStudySubtitle,
hero: caseStudy.node.caseStudyHero,
introTitle: caseStudy.node.caseStudyIntroTitle,
intro: caseStudy.node.caseStudyIntro.caseStudyIntro,
link: caseStudy.node.caseStudyLink,
caseStudySection: caseStudySectionMatches.node
}
})
})
})
// This is the error handling for the calls
.catch((errors) => {
console.log(errors)
reject(errors)
})
) // close resolve handler
}) // close promise
}

I ran into this challenge too and couldn't find a good solution to accomplish that (although I wasn't using Contentful), but I did work past it and think I can help. You'll need to shift your thinking a bit.
Basically, GraphQL isn't really meant to query for the data you need to run another query. It's more of a 'ask for what you need' sort of tool. GraphQL wants to run a single query for exactly what you need.
The parameter you need for your query actually comes from your gatsby-node.js file. Specifically, the context property of createPages()(a Node API that gatsby makes available).
Is that enough to get you pointed in the right direction? If you need a bit more of a hand, then there are two things I need to know:
1. A little more context around what you're trying to accomplish. What is the specific data you want available to the end user?
2. What your gatsby-node.js file looks like.

Short answer: you don't do callbacks with GraphQL. You do one query that gets everything you need all at once.
Longer answer: I had to reconstruct how the gatsby-node.js file fetched Contentful content and then filtered through it. In Gatsby, you want to set up the queries in gatsby-node.js to go fetch everything from your data source because it's a static site generator. It's architecture brings all that data in and then parses it out accordingly.
The GraphQL query from my original question was fine. I changed .then() of the promise to use .filter() on my results, comparing the relationship field of the child nodes to the id of the parent nodes.
gatsby-node.js:
// Set Gatsby path up to be used by .createPages
const path = require('path')
// Using Node's module export, Gatsby adds in a createPages factory
exports.createPages = ({ graphql, actions }) => {
// We setup the createPage function takes the data from the actions object
const { createPage } = actions
// Setup a promise to build pages from contentful data model for bigCaseStudies
return new Promise((resolve, reject) => {
// Setup destination component for the data
const bigCaseStudyComponent = path.resolve('src/components/BigCaseStudy/bigcasestudy.js')
resolve(
graphql(`
{
allContentfulBigCaseStudy {
edges {
node {
id
caseStudySlug
caseStudyTitle
caseStudySubtitle
caseStudyIntroTitle
caseStudyIntro {
caseStudyIntro
}
caseStudyLink
caseStudyHero {
fixed {
width
height
src
srcSet
}
}
}
}
}
allContentfulBigCaseStudySection {
edges {
node {
title
order
images {
fixed {
width
height
src
srcSet
}
}
bigCaseStudyReference {
id
}
body {
body
}
stats {
stat1 {
word
number
}
stat2 {
word
number
}
stat3 {
word
number
}
stat4 {
word
number
}
}
id
}
}
}
}
`).then((result) => {
// Now we loop over however many caseStudies Contentful sent back
result.data.allContentfulBigCaseStudy.edges.forEach((caseStudy) => {
let matchedCaseStudySections = result.data.allContentfulBigCaseStudySection.edges.filter(
caseStudySection =>
caseStudySection.node.bigCaseStudyReference.id === caseStudy.node.id
)
createPage ({
path: `/work/${caseStudy.node.caseStudySlug}`,
component: bigCaseStudyComponent,
context: {
id: caseStudy.node.id,
slug: caseStudy.node.caseStudySlug,
title: caseStudy.node.caseStudyTitle,
subtitle: caseStudy.node.caseStudySubtitle,
hero: caseStudy.node.caseStudyHero,
introTitle: caseStudy.node.caseStudyIntroTitle,
intro: caseStudy.node.caseStudyIntro.caseStudyIntro,
link: caseStudy.node.caseStudyLink,
caseStudySection: matchedCaseStudySections.node
}
})
})
})
// This is the error handling for the calls
.catch((errors) => {
console.log(errors)
reject(errors)
})
) // close resolve handler
}) // close promise
}
Once you've set this up, the createPage part of Gatsby Node API sends the parent and all of it's nodes over to the component param you set.
Inside of my component, I can now make a GraphQL query for all children nodes. That now returns what I want and conforms to the idea that GraphQL makes one request instead of multiple like I was trying to do. The only tricky part is that you have to use .map() in the render part of the component to loop over all the child nodes sent back from Contentful.
bigcasestudy.js component:
import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import { graphql } from 'gatsby'
import Img from 'gatsby-image'
import Layout from '../Layout/layout'
/**
* Hero Section
*/
const HeroContainer = styled.header`
align-items: center;
background-image: url(${ props => props.bgImgSrc });
background-position: center center;
background-size: cover;
display: flex;
flex-direction: column;
justify-content: center;
height: calc(100vh - 128px);
`
const HeroTitle = styled.h1`
color: #fff;
font-size: 70px;
font-weight: 700;
letter-spacing: 2px;
margin-bottom: 15px;
`
const HeroSubtitle = styled.h2`
color: #fff;
font-size: 24px;
font-weight: 300;
letter-spacing: 5px;
text-transform: uppercase;
`
/**
* Intro Section
*/
const IntroBG = styled.section`
background-color: ${ props => props.theme.lightGray };
padding: 50px 0;
`
const IntroContainer = styled.div`
padding: 25px;
margin: 0 auto;
max-width: ${ props => props.theme.sm };
#media (min-width: ${ props => props.theme.sm }) {
padding: 50px 0;
}
`
const IntroTitle = styled.h2`
font-size: 50px;
font-weight: 700;
letter-spacing: 2px;
margin-bottom: 45px;
text-align: center;
`
const IntroText = styled.p`
font-size: 22px;
line-spacing: 4;
text-align: center;
`
const IntroButton = styled.a`
background-color: #fff;
color: ${ props => props.theme.darkGray };
border: 1px solid ${ props => props.theme.mediumGray };
border-radius: 25px;
display: block;
font-size: 16px;
letter-spacing: 5px;
margin: 30px auto;
padding: 15px 45px;
text-align: center;
text-decoration: none;
text-transform: uppercase;
width: 300px;
`
// BigCaseStudy Component
class BigCaseStudy extends React.Component {
render() {
// Destructure Case Study Intro stuff
const {
caseStudyHero,
caseStudyIntro,
caseStudyIntroTitle,
caseStudyLink,
caseStudySubtitle,
caseStudyTitle
} = this.props.data.contentfulBigCaseStudy
// Setup references to Case Study Sections, destructure BigCaseStudySection object
const caseStudySections = this.props.data.allContentfulBigCaseStudySection.edges.map(
(currentSection) => {
return currentSection.node
}
)
// Case Study Section can be in any order, so we need to sort them out
const caseStudySectionsSorted = caseStudySections.sort( (firstItem, secondItem) => {
return firstItem.order > secondItem.order ? 1 : -1
})
console.log(caseStudySectionsSorted)
return (
<Layout>
<HeroContainer
bgImgSrc={ caseStudyHero.fixed.src }>
<HeroTitle>{ caseStudyTitle }</HeroTitle>
<HeroSubtitle>{ caseStudySubtitle }</HeroSubtitle>
</HeroContainer>
<IntroBG>
<IntroContainer>
<IntroTitle>{ caseStudyIntroTitle }</IntroTitle>
<IntroText>{ caseStudyIntro.caseStudyIntro }</IntroText>
</IntroContainer>
<IntroButton href={ caseStudyLink } target="_blank" rel="noopener noreferrer">
Visit the site >
</IntroButton>
</IntroBG>
{
caseStudySectionsSorted.map( (caseStudySection, index) => {
return <IntroTitle key={ index }>{ caseStudySection.title }</IntroTitle>
})
}
</Layout>
)
}
}
// Confirm data coming out of contentful call is an object
BigCaseStudy.propTypes = {
data: PropTypes.object.isRequired
}
// Export component
export default BigCaseStudy
// Do call for the page data
// This needs to mirror how you've set up the dynamic createPage function data in gatsby-node.js
export const BigCaseStudyQuery = graphql`
query BigCaseStudyQuery {
contentfulBigCaseStudy {
id
caseStudyTitle
caseStudySubtitle
caseStudyIntroTitle
caseStudyIntro {
caseStudyIntro
}
caseStudyLink
caseStudyHero {
fixed {
width
height
src
srcSet
}
}
}
allContentfulBigCaseStudySection {
edges {
node {
title
order
images {
fixed {
width
height
src
srcSet
}
}
bigCaseStudyReference {
id
}
body {
body
}
stats {
stat1 {
word
number
}
stat2 {
word
number
}
stat3 {
word
number
}
stat4 {
word
number
}
}
id
}
}
}
}
`
H/t: thanks to #taylor-krusen for rearranging how I was approaching this problem.

Related

Switch between CSS variables based on states

I'm trying to implement color-themes into my react app and went with css-variables and the basics work out so far.
However I'm also trying to apply the theme to an SVG icon that changes the color based on a state. It goes from grey to a color which is defined by the theme when a phone gets connected to a device.
I have a themes.css which looks like this:
:root {
--fillInactive: #7c7c7c;
}
[data-theme='Blue'] {
--fillHover: #708fa8;
--fillActive: #3f77a4;
}
[data-theme='Red'] {
--fillHover: #cd6969;
--fillActive: #cd2424;
}
[data-theme='Green'] {
--fillHover: #85b08e;
--fillActive: #41ad56;
}
[data-theme='White'] {
--fillHover: #9f9f9f;
--fillActive: #ffffff;
}
My topbar.js looks like this:
import React from "react";
import { useState, useEffect } from "react";
import "./topbar.scss";
import "../components/themes.scss"
const electron = window.require('electron');
const { ipcRenderer } = electron;
const TopBar = () => {
const Store = window.require('electron-store');
const store = new Store();
const [phoneState, setPhoneState] = useState("#7c7c7c");
const [theme, setTheme] = useState(store.get("colorTheme"));
const plugged = () => {
setPhoneState("#3f77a4");
}
const unplugged = () => {
setPhoneState("#7c7c7c");
}
useEffect(() => {
ipcRenderer.send('statusReq');
ipcRenderer.on("plugged", plugged);
ipcRenderer.on("unplugged", unplugged);
return function cleanup() {
ipcRenderer.removeListener('plugged', plugged);
ipcRenderer.removeListener('unplugged', unplugged);
};
}, []);
return (
<div className="topbar" data-theme={theme}}>
<div className="topbar__info">
<svg className="topbar__icon">
<use xlinkHref="./svg/phone.svg#phone" color={phoneState}></use>
</svg>
</div>
</div>
);
};
export default TopBar;
My TopBar.scss looks like this:
.topbar {
display: flex;
flex-direction: row;
justify-content: space-between;
position: absolute;
top: 0;
height: 66px;
width: 100%;
background: #000000;
&__info {
margin-left: 3rem;
}
&__icon {
&__phone {
}
margin-top: 1rem;
margin-right:1rem;
width: 1rem;
height: 1rem;
}
}
For now I hardcoded a blue color into my JS, but would like that the property "--fillActive" is set for the SVG when the phone is plugged, based on the chosen Theme.
When the phone is unplugged it should switch to "--fillInactive" so it greys out again.
I hope my question is specific enough. I tried googling for a solution, but I'm not even sure that what I'm trying to do is the right way, hence trying to find a good search query is rather difficult. I saw some solutions where you can just override a variable through javascript, but I would like that the properties stay as they are and that I just choose between the two to set the correct color based on the state.
Any help or hint is highly appreciated.
As an idea, the phoneState could contain the actual phone state: plugged/unplugged:
const [phoneState, setPhoneState] = useState(() => 'unplugged');
Then the corresponding callback would toggle the state:
useEffect(() => {
ipcRenderer.send('statusReq');
ipcRenderer.on("plugged", setPhoneState(() => 'plugged'));
ipcRenderer.on("unplugged", setPhoneState(() => 'unplugged'));
return function cleanup() { ... },
[]);
In this case the phoneState can be used for a dynamic class of some element, for example:
return (
<div className="topbar" data-theme={theme}}>
<div className={`topbar__info topbar__info--${phoneState}`}>
<svg className="topbar__icon">
<use xlinkHref="./svg/phone.svg#phone" ></use>
</svg>
</div>
</div>
);
Then when the css vars are applied, they would have corresponding values according to the active theme.
.topbar {
&__info{
&--plugged{
color: var(--fillActive);
}
&--unplugged{
color: var(--fillInactive);
}
}
}

Angular select custom control and Selenium Webdriver

I am a developer working with my organization's BQA group assisting with their test automation. We have a big and complicated web site developed in Angular (version 7 to 10, I think) whose development has been contracted out to another large firm. I am trying to implement Selenium Webdriver using Visual Studio and C# with NUnit. One if my biggest pains with the automation has been with a custom implementation the drop-down lists.
I know a little JavaScript but have been trying to catch up with ES5, ES6, NodeJS, TypeScript, Angular, etc. However, I am better with C#.
I can easily use the built-in functions within Selenium to select the options within these lists. However, these values never get registered server-side and often trigger warnings if they are marked as mandatory.
var driver = new ChromeDriver();
driver.FindElement(By.XPath("//app-select[#id='myId']")).Click();
new SelectElement(driver.FindElement(By.XPath("//app-select[#id='myId']//select"))).SelectByText("Some Option");
or even...
var js = (IJavaScriptExecutor)driver;
var appSelect = new SelectElement(driver.FindElement(By.XPath("//app-select[#id='myId']//select")));
foreach(var opt in appSelect.Options)
{
if(opt.Text = "Some Option")
{
js.ExecuteScript("arguments[0].selected=true", opt); // Works like SelectByText().
js.ExecuteScript("arguments[0].input", appSelect); // These don't seem to ...
js.ExecuteScript("arguments[0].change", appSelect); // ... make any kind of difference.
break;
}
}
I have access to the code-base of our web site. So, I can examine this custom component but I do not understand Angular very well. I suspect there is a server-side session variable (NgModel?) that is not being updated. I don't know if I can use client-side JavaScript to trigger an update and make my scripts behave properly.
Any suggestions would be most appreciated!
Here is the Angular code for the component.
===== select.component.html =====
<select #innerSelect [attr.id]="uid" class="select" [value]="value" [disabled]="disabled" (blur)="onBlur()"
(input)="inputChanged($event)">
<ng-content></ng-content>
</select>
<div class="arrow-icon" aria-hidden="true"></div>
===== select.component.ts =====
import { Component, ViewEncapsulation, forwardRef, Output, EventEmitter, Input, OnInit, Renderer2, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '#angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '#angular/forms';
import { ContentObserver } from '#angular/cdk/observers';
import { Subscription } from 'rxjs';
let nextUniqueId = 0;
/** A wrapper for a native select component. This allows us to render out a drop down arrow that is inline with branding. */
#Component({
selector: 'app-select',
templateUrl: './select.component.html',
styleUrls: ['./select.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SelectComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class SelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy {
/** When dataType is set to "number",. value will be converted to a number on get. */
#Input() dataType;
/** Whether the component is disabled. */
#Input() disabled = false;
/** Value of the control. */
#Input()
get value(): any { return this._value; }
set value(newValue: any) {
if (newValue !== this._value) {
this.writeValue(newValue);
this._value = newValue;
}
}
private _value: any;
/** Unique id of the element. */
#Input()
get uid(): string { return this._uid; }
set uid(value: string) {
this._uid = value || this._defaultId;
}
private _uid: string;
/** tracks changes to the content of the inner select (options) */
private contentChanges: Subscription = null;
/** appAutoFocus directive does not work here, so we implement autoFocus ourselves */
#Input() autoFocus = false;
/**
* Event that emits whenever the raw value of the select changes. This is here primarily
* to facilitate the two-way binding for the 'value' input.
*/
#Output() readonly valueChange: EventEmitter<any> = new EventEmitter<any>();
#ViewChild('innerSelect', { static: true }) innerSelect: ElementRef;
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/** Unique id for this input. */
// tslint:disable-next-line:member-ordering
private _defaultId = `app-select-${nextUniqueId++}`;
/** View -> model callback called when value changes */
onChange: (value: any) => void = () => { };
/** View -> model callback called when select has been touched */
onTouched = () => { };
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
constructor(private el: ElementRef, private renderer: Renderer2, private obs: ContentObserver) {
// Force setter to be called in case id was not specified.
this.uid = this.uid;
}
ngOnInit() {
this.autoFocus = this.el.nativeElement.hasAttribute('autoFocus');
}
ngAfterViewInit() {
// Watch for changes in the content of the select control.
// This is in place for situations where the list of options comes in *after* the value of the select has been set.
// If that occurs, re-set the value of the select to force the browser to pick the right option in the drop down list.
this.contentChanges = this.obs.observe(this.innerSelect.nativeElement).subscribe((event: MutationRecord[]) => {
this.innerSelect.nativeElement.value = this._value;
});
if (this.autoFocus === true) {
setTimeout(() => {
this.innerSelect.nativeElement.focus();
}, 200);
}
if (this.el.nativeElement.hasAttribute('id')) {
console.warn('app-select has an "id". Use "uid" to set the id of the inner select element.');
}
}
ngOnDestroy() {
this.contentChanges.unsubscribe();
}
/**
* Sets the select's value. Part of the ControlValueAccessor interface
* required to integrate with Angular's core forms API.
*/
writeValue(value: any): void {
this._value = value;
}
/**
* Saves a callback function to be invoked when the select's value
* changes from user input. Part of the ControlValueAccessor interface
* required to integrate with Angular's core forms API.
*/
registerOnChange(fn: (value: any) => void): void {
this.onChange = fn;
}
/**
* Saves a callback function to be invoked when the select is blurred
* by the user. Part of the ControlValueAccessor interface required
* to integrate with Angular's core forms API.
*/
registerOnTouched(fn: () => {}): void {
this.onTouched = fn;
}
/**
* Disables the select. Part of the ControlValueAccessor interface required
* to integrate with Angular's core forms API.
*/
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
onBlur() {
this.onTouched();
}
inputChanged(event) {
const targetValue = event.target.value;
if (targetValue && targetValue.toLowerCase() === 'null') {
this.value = null;
} else if (this.dataType === 'number') {
// if number is specified, attempt to convert the value
this.value = +targetValue;
} else if (this.dataType === 'string') {
// if string is specified, attempt to convert the value
this.value = '' + targetValue;
} else if (targetValue === '') {
// treat empty string as null
this.value = null;
} else if (!isNaN(targetValue)) {
// if no dataType is specified, attempt to determine if the value is a number
this.value = +targetValue;
} else {
// otherwise use the value as is (should be a string)
this.value = targetValue;
}
this.onChange(this.value);
}
}
===== select.component.scss =====
app-select {
position: relative;
display: block;
width: 19em; // default drop-down width?
select {
display: inline-block;
position: relative;
width: 100%;
padding: .37em .4em;
padding-right: 44px;
font-size: 1.125em;
color: #222;
border: 1px solid #000;
border-radius: 4px;
background: white;
height: 38px;
background: linear-gradient(to left, #ffcd41 0, #ffcd41 21px, #484848 21px, white 22px);
box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.5);
transition: border linear 0.2s, box-shadow linear 0.2s;
text-overflow: ellipsis;
option {
color: #222;
font-size: .9em;
padding: .2em;
&.default {
font-weight: 600;
color: black;
}
&.legacy {
color: #333;
}
}
optgroup {
&.legacy {
background: #eeeeee;
color: #333;
}
}
&[readonly] {
border-color: #bbb;
color: black;
background-color: #f7f7f7;
-webkit-appearance: none; // hide drop down arrow
}
&.ng-dirty {
color: black;
}
&[disabled]:not([readonly]) {
background: #E6E6E6;
opacity: 1;
}
}
.arrow-icon {
display: block;
position: absolute;
right: 1px;
top: 1px;
bottom: 1px;
width: 44px;
background: #ffcd41;
border-left: 1px solid #000;
pointer-events: none;
border-radius: 0 4px 4px 0;
padding-top: 7px;
padding-left: 11px;
font-size: 1.4em;
line-height: normal;
&:before {
font-family: "custom" !important;
content: "\71";
}
}
.select[disabled]:not([readonly])+.arrow-icon {
background-color: #E6E6E6;
}
.select:focus+.arrow-icon {
right: 2px;
top: 2px;
bottom: 2px;
width: 43px;
padding-top: 6px;
}
.select:focus-within {
box-shadow: inset 0 0 0 1px #000;
}
}
After more research I discovered the dispatchEvent() function.
This is my solution to my own question:
var driver = new ChromeDriver();
var js = (IJavaScriptExecutor)driver;
var select = baseDriver.FindElement(By.XPath("//select"));
string script = "const evt = new Event('input', {'bubbles':true, 'cancelable':true});" +
"arguments[0].dispatchEvent(evt);";
js.ExecuteScript(script, select);
Also, this still works (JavaScript portion):
const evt = document.createEvent("HTMLEvents");
evt.initEvent("input", true, true);
arguments[0].dispatchEvent(evt);
I hope someone finds this useful.

Checking aria with Jest/Enzyme

I want to check if aria-expanded changes after a button click. This is my component
import React, { useState, useRef } from 'react';
import styled from 'styled-components';
import { ArrowTemplate } from './ArrowTemplate';
import { colors } from '../../utils/css';
import { Text } from '../Text';
const Accordion = ({
rtl, content, color, title, className,
}) => {
const element = useRef(null);
const [isAccordionExpanded, setIsAccordionExpanded] = useState(false);
const toggleAccordion = () => {
setIsAccordionExpanded(!isAccordionExpanded);
};
const height = element.current ? element.current.scrollHeight : '0';
return (
<div className={`accordion-section ${className}`}>
<button className={'accordion-btn'} onClick={toggleAccordion}>
<p className={'accordion-title'}>
<Text isRtl={rtl}>{title}</Text>
</p>
<ArrowTemplate
direction={isAccordionExpanded ? 'up' : 'down'}
onClick={toggleAccordion}
rtl={rtl}
color={color}
/>
</button>
<AccordionContent
className={'accordion-content'}
height={height}
isAccordionExpanded={isAccordionExpanded}
ref={element}
aria-expanded={isAccordionExpanded}
>
<div className={'accordion-text'}>
<Text isRtl={rtl}>{content}</Text>
</div>
</AccordionContent>
</div>
);
};
export const StyledAccordion = styled(Accordion)`
font-family: "Open Sans", sans-serif;
text-align: ${({ rtl }) => (rtl ? 'right' : 'left')};
display: flex;
flex-direction: column;
.accordion-btn {
position: relative;
width: 100%;
background-color: ${colors.LG_GREY_4};
color: ${colors.LG_GREY_5};
cursor: pointer;
padding: 40px 18px;
display: flex;
align-items: center;
border: none;
outline: none;
:hover,
:focus,
:active {
background-color: ${colors.LG_GREY_6};
}
.accordion-title {
${({ rtl }) => (rtl ? 'right: 50px;' : 'left: 50px;')};
position: absolute;
font-weight: 600;
font-size: 14px;
}
}
`;
export const AccordionContent = styled.div`
max-height: ${({ isAccordionExpanded, height }) => (isAccordionExpanded ? height : '0')}px;
overflow: hidden;
background: ${colors.LG_GREY_7};
transition: max-height 0.7s;
.accordion-text {
font-weight: 400;
font-size: 14px;
padding: 18px;
}
`;
Here are my tests
import React from 'react';
import { shallow, configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { StyledAccordion, AccordionContent } from '../Accordion';
configure({ adapter: new Adapter() });
describe('<StyledAccordion/>',
() => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<StyledAccordion/>);
});
it('should match the snapshot', () => {
expect(wrapper).toMatchSnapshot();
});
it('should originally have aria-expanded set to false', () => {
expect(wrapper.find(AccordionContent).props()['aria-expanded']).toBe(false);
});
it('should set aria-expanded to true onClick', () => {
wrapper.find('.accordion-btn').simulate('click');
expect(wrapper.find(AccordionContent).props()['aria-expanded']).toBe(true);
});
});
And here's what I'm getting in the console
FAIL src/components/Accordion/test/Accordion.test.js (21.497s)
● <StyledAccordion/> › should originally have aria-expanded set to false
Method “props” is meant to be run on 1 node. 0 found instead.
16 | });
17 | it('should originally have aria-expanded set to false', () => {
> 18 | expect(wrapper.find(AccordionContent).props()['aria-expanded']).toBe(false);
| ^
19 | });
20 | it('should set aria-expanded to true onClick', () => {
21 | wrapper.find('.accordion-btn').simulate('click');
at ShallowWrapper.single (node_modules/enzyme/src/ShallowWrapper.js:1636:13)
at ShallowWrapper.single [as props] (node_modules/enzyme/src/ShallowWrapper.js:1160:17)
at Object.props (src/components/Accordion/test/Accordion.test.js:18:45)
● <StyledAccordion/> › should set aria-expanded to true onClick
Method “simulate” is meant to be run on 1 node. 0 found instead.
19 | });
20 | it('should set aria-expanded to true onClick', () => {
> 21 | wrapper.find('.accordion-btn').simulate('click');
| ^
22 | expect(wrapper.find(AccordionContent).props()['aria-expanded']).toBe(true);
23 | });
24 | });
at ShallowWrapper.single (node_modules/enzyme/src/ShallowWrapper.js:1636:13)
at ShallowWrapper.single [as simulate] (node_modules/enzyme/src/ShallowWrapper.js:1118:17)
at Object.simulate (src/components/Accordion/test/Accordion.test.js:21:38)
How can I test attributes?
Checking attribute by verifying props is fine. Simulating click is fine.
The only cause test fails is how shallow() works under the hood.
You may figure out that by yourselves by checking what wrapper.debug() returns(say by adding console.log(wrapper.debug()))
You will see something like <StyledComponentConsumer><Component ...></StyledComponentConsumer>. So the reason is shallow() does not render nested component. Say, if there were no styled-components and you tried to shallow(<Accordion />).find('span')(assuming that <Text> should be rendered as <span>) you will never find that as well.
First solution may be using mount() instead of shallow()(you should not even need to change test). But I would like to go different way(just an opinion though: https://hackernoon.com/why-i-always-use-shallow-rendering-a3a50da60942)
Read more about shallow vs mount difference at https://medium.com/#Yohanna/difference-between-enzymes-rendering-methods-f82108f49084
Another approach is to use .dive() like
wrapper.dive().find(...)
or even at initialization time:
const wrapper = shallow(...).dive();
And finally you may just export two versions: base one(and write tests against that) and wrapped into styled-components theming. Needs slightly more changes in code. And dislike approaches above tests will not require updates each time you add more HOC wrappers(say wrapping component into styled-components, then connect to redux and finally adding withRouter - tests will still work the same this way). But from the other side this approach does not test your component as it's actually integrated so I'm not really sure if this approach is better.

DOM element becomes Null in Vue project

I'm working on a code typer, but for some reason the element becomes Null with no reason I can make out. I am new to Vue & know this code works as I've previously completed this at https://CodeSpent.dev (live preview) in Django/Python until I determined it'd be valuable to learn more front end frameworks.
So I believe it has something to do with how Vue handles rendering, but I'm only a few hours into learning & have no idea where to even look with this.
Here is the code:
var codeBlock = document.getElementById('code')
console.log(codeBlock)
setTimeout(() => {
new TypeIt(codeBlock, {
strings: [codeSample],
speed: 20
}).go();
}, 1000)
setInterval(function () {
const code = Prism.highlight(codeBlock.innerText, Prism.languages.python, 'python');
document.getElementById('real-code').innerHTML = code;
}, 10);
If we look at console we can see on line 23 where codeBlock is clearly not null, but then when we try to use it it becomes null. Anything stand out?
Full Component:
<template>
<div id="code-block" class="bb">
<pre class="code-pre">
<code id="real-code"></code>
</pre>
<div id="code" class="language-py"></div>
</div>
</template>
<script>
import 'prismjs'
import 'prismjs/themes/prism.css'
import TypeIt from 'typeit';
export default {
name: 'CodeTyper'
}
var codeSample = '\x0a\x3E\x3E\x20\x6E\x61\x6E\x6F\x20\x63\x6F\x64\x65\x73\x70\x65\x6E\x74\x2E\x70\x79\x0A\x66\x72\x6F\x6D\x20\x70\x79\x74\x68\x6F\x6E\x20\x69\x6D\x70\x6F\x72\x74\x20\x44\x65\x76\x65\x6C\x6F\x70\x65\x72\x0A\x66\x72\x6F\x6D\x20\x70\x6F\x72\x74\x66\x6F\x6C\x69\x6F\x2E\x6D\x6F\x64\x65\x6C\x73\x20\x69\x6D\x70\x6F\x72\x74\x20\x50\x6F\x72\x74\x66\x6F\x6C\x69\x6F\x0A\x0A\x63\x6C\x61\x73\x73\x20\x43\x6F\x64\x65\x53\x70\x65\x6E\x74\x28\x44\x65\x76\x65\x6C\x6F\x70\x65\x72\x29\x3A\x0A\x20\x20\x20\x20\x6E\x61\x6D\x65\x20\x3D\x20\x27\x50\x61\x74\x72\x69\x63\x6B\x20\x48\x61\x6E\x66\x6F\x72\x64\x27\x0A\x20\x20\x20\x20\x6C\x6F\x63\x61\x74\x69\x6F\x6E\x20\x20\x3D\x20\x27\x50\x69\x74\x74\x73\x62\x75\x72\x67\x68\x2C\x20\x50\x41\x2C\x20\x55\x53\x27\x0A\x20\x20\x20\x20\x6C\x61\x6E\x67\x75\x61\x67\x65\x73\x20\x3D\x20\x5B\x27\x70\x79\x74\x68\x6F\x6E\x27\x2C\x20\x27\x6A\x61\x76\x61\x73\x63\x72\x69\x70\x74\x27\x2C\x20\x27\x63\x73\x73\x27\x2C\x27\x68\x74\x6D\x6C\x35\x27\x5D\x0A\x20\x20\x20\x20\x66\x61\x76\x6F\x72\x69\x74\x65\x73\x20\x3D\x20\x5B\x27\x64\x6A\x61\x6E\x67\x6F\x27\x2C\x20\x27\x74\x65\x6E\x73\x6F\x72\x66\x6C\x6F\x77\x27\x2C\x20\x27\x74\x77\x69\x74\x63\x68\x27\x2C\x20\x27\x64\x69\x73\x63\x6F\x72\x64\x27\x2C\x20\x27\x6F\x70\x65\x6E\x63\x76\x27\x5D\x0A\x0A\x20\x20\x20\x20\x64\x65\x66\x20\x5F\x5F\x73\x74\x72\x5F\x5F\x28\x73\x65\x6C\x66\x29\x3A\x0A\x20\x20\x20\x20\x20\x20\x72\x65\x74\x75\x72\x6E\x20\x73\x65\x6C\x66\x2E\x6E\x61\x6D\x65'
var codeBlock = document.getElementById('code')
console.log(codeBlock)
setTimeout(() => {
new TypeIt(codeBlock, {
strings: [codeSample],
speed: 20
}).go();
}, 1000)
setInterval(function () {
const code = Prism.highlight(codeBlock.innerText, Prism.languages.python, 'python');
document.getElementById('real-code').innerHTML = code;
}, 10);
</script>
<style>
#real-code {
color: #5c5edc;
}
#code-block {
background-color: #141D22;
color: #fff;
flex: 1;
height: 355px;
}
#code-block-sub {
background-color: rgb(34, 32, 35);
color: #fff;
width: 100%;
padding: 0 15px;
height: 150px;
}
#code,
#code-sub {
padding: 0px !important;
margin: 0px !important;
display: none;
color: #fff !important;
}
</style>
First a template that presents the partial string...
<template>
<div>
<pre>{{partialCode}}</pre>
<v-btn #click="startAppending()"></v-btn>
</div>
</template>
Then the partialCode string bound into data...
export default {
data () {
return {
partialCode: '',
// other data
}
},
You may want to start appending onCreate or some other lifecycle hook (or once you receive the code data asynchronously), but the key to the logic is that you can now just change the state of partialCode and let the DOM update itself...
methods: {
startAppending() {
this.partialCode = '' // start fresh each time
const code = Prism.highlight(codeBlock.innerText, Prism.languages.python, 'python')
let index = 0
let interval = setInterval(() => {
if (this.partialCode.length === code.length) {
clearInterval(interval)
} else {
this.partialCode = code.slice(0, index++)
}
}, 200);
},
// the other methods
}

How to conditionally add attributes to react DOM element

I have a scenario where I'm using React.js to create a div using the following code :
React.createElement('div', {}, "div content")
Some additional javascript processing will allow me afterwards to deduce if this div needs to have the className attribute set to" ClassA" or "ClassB" or if it shouldn't have className at all.
Is there a way in javascript to access the div that was created from the React DOM and to add to it the className attribute?
Note : I couldn't achieve this is JSX so I resorted to the createElement method.
Edit: it is worth to mention that i might need to conditionally add attributes other than className. For example, I might need to add to an anchor tag an "alt" attribute or not based on conditional logic.
Thank you in advance.
Use JSX spread. Build and object with props, modify it however you like and pass it to component like so:
const props = {
name: 'SomeName'
}
if (true) {
props.otherName = 'otherName';
}
return (
<SomeComponent {...props}/>
);
See that ... syntax? That spread operator do the job - all props will end up as separate attributes on your component.
Take a look at this plunk: http://www.webpackbin.com/4JzKuJ9C-
Since you were trying to initially have your logic in JSX. I have a jsx solution that uses state
class App extends React.Component {
constructor() {
super();
this.state = {
classValue: ''
}
}
handleClick = () => {
if(this.state.classValue == '') {
this.setState({classValue : 'green'});
}
else if(this.state.classValue == 'green') {
this.setState({classValue : 'blue'});
}
else {
this.setState({classValue : 'green'});
}
}
render() {
return (
<div>
<div className={this.state.classValue}>Hello World</div>
<button onClick={this.handleClick()}>Toggle</button>
</div>
)}
}
ReactDOM.render(<App/>, document.getElementById('app'));
.green {
background-color: green;
}
.blue {
background-color: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.0/react-dom.min.js"></script>
<div id="app"></div>
The example shows how can you change your className similarly using state you can set whatever attributes you like to change.
You can use ES6 syntax to achieve the desired functionality
const yourComponentProps = {
[ifThisIsTrue ? 'useThisName' : 'useAnotherName']: 'yourDesiredValue',
};
return <YourComponent {...yourComponentProps} />
This is a quite normal situation in React and requires virtually no special handling.
Note: It is best to hand props down the component tree declaratively but if that is not an option you can bind listener functions in componentDidMount and unbind them in componentWillUnmount as shown in the following example. So long as they call setState, your component's render function will get triggered.
const { Component, cloneElement } = React
class Container extends Component {
constructor(props) {
super(props)
this.state = { classNames: [ 'foo' ] }
this.appendClassName = () => {
const { classNames } = this.state
this.setState({ classNames: [ ...classNames, `foo_${classNames.length}` ] })
}
}
componentDidMount() {
document.querySelector('button').addEventListener('click', this.appendClassName)
}
componentWillUnmount() {
document.querySelector('button').removeEventListener('click', this.appendClassName)
}
render() {
const { children } = this.props
const { classNames } = this.state
return <div className={classNames.join(' ')}>{children}</div>
}
}
ReactDOM.render(<Container>I am content</Container>, document.getElementById('root'))
.foo {
font-family: monospace;
border: 1px solid rgb(100, 50, 50);
font-size: 1rem;
border-style: solid;
border-width: 1px;
width: 50vw;
height: 50vh;
margin: auto;
display: flex;
align-self: center;
justify-content: center;
align-items: center;
}
.foo.foo_1 {
font-size: 1.5rem;
background-color: rgb(200, 100, 200);
}
.foo.foo_2 {
font-size: 2rem;
border-radius: 3px 7px;
background-color: rgb(180, 120, 200);
}
.foo.foo_3 {
border-style: dashed;
background-color: rgb(175, 130, 180);
}
.foo.foo_4 {
border-width: 2px;
background-color: rgb(160, 165, 170);
}
.foo.foo_5 {
border-width: 1rem;
background-color: rgb(150, 200, 150);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<button>Click me</button>
<div id="root"></div>
P.S. - Avoid using componentWillMount, it can lead to bugs in the lifecycle and there is talk that it may be removed in a future version of React. Always make async side-effect laden requests within componentDidMount and clean them up in componentWillUnmount. Even if you have nothing to render, you are best off rendering a placeholder component until your data arrives (best option for fast loading), or nothing at all.

Categories

Resources