DOM element becomes Null in Vue project - javascript

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
}

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);
}
}
}

Vue multiple components and access to Vuex properties

I'm learning Vue, using it with Vuex (without Webpack), but I have several questions when implementing this simple example, it's not clear for me in the docs.
Don't know why, but, I can't access the Vuex store using this pointer inside component computed property, for example:
this.$store.state.nav.title, leading me to use global app variable instead. Also, this.$parent and $root do not work.
Is it correct to initialize multiple Vue components at one time such as this, and shouldn't they have been mounted automatically when I pass components property to the Vue construct object? What is the right way to initialize, for example, the header, footer and body components at the same time?
var app = new Vue({
el: document.getElementById('app'),
data: {
title:store.state.nav.title
},
computed: {},
methods:{},
mounted:function(){},
updated:function(){},
store:store,
components:{
componentheader,
componentnavbar,
componentbody,
componentfooter
}
});
for (var companent_name in app.$root.$options.components) {
if(typeof app.$root.$options.components[companent_name] === 'function') {
var MyComponent = Vue.extend(app.$root.$options.components[companent_name]);
var component = new MyComponent().$mount();
document.getElementById('app').appendChild(component.$el);
}
}
Here is the full example:
var store = new Vuex.Store({
state: {
nav: {
title: 'my site'
}
},
mutations: {
changeTitle: function(t, a) {
this.state.nav.title = a;
}
}
});
var componentheader = Vue.component('componentheader', {
computed: {
title() {
return app.$store.state.nav.title
}
},
template: '#header_tpl',
mounted: function() {},
updated: function() {}
});
var componentnavbar = Vue.component('componentnavbar', {
computed: {
title() {
return app.$store.state.nav.title
}
},
template: '#navbar_tpl',
mounted: function() {},
updated: function() {}
});
var componentbody = Vue.component('componentbody', {
computed: {
title() {
return app.$store.state.nav.title
}
},
template: '#body_tpl',
mounted: function() {},
updated: function() {}
});
var componentfooter = Vue.component('componentfooter', {
computed: {
title() {
return app.$store.state.nav.title
}
},
template: '#footer_tpl',
mounted: function() {},
updated: function() {}
});
var app = new Vue({
el: document.getElementById('app'),
data: {
title: store.state.nav.title
},
computed: {},
methods: {},
mounted: function() {},
updated: function() {},
store: store,
components: {
componentheader,
componentnavbar,
componentbody,
componentfooter
}
});
Vue.use(Vuex);
for (var companent_name in app.$root.$options.components) {
if (typeof app.$root.$options.components[companent_name] === 'function') {
var MyComponent = Vue.extend(app.$root.$options.components[companent_name]);
var component = new MyComponent().$mount();
document.getElementById('app').appendChild(component.$el);
}
}
Vue.config.devtools = false;
Vue.config.productionTip = false;
* {
margin: 0;
padding: 0;
color: #fff;
text-align: center;
font-size: 19px;
}
html,
body,
.container {
height: 100%;
}
#app {
position: relative;
height: 100%;
min-height: 100%;
}
header {
width: 100%;
height: 80px;
}
nav.navbar {
box-sizing: border-box;
min-height: 100%;
padding-bottom: 90px;
width: 80px;
height: 100%;
position: absolute;
}
.container {
box-sizing: border-box;
min-height: 100%;
padding-bottom: 90px;
color: #000;
}
footer {
height: 80px;
margin-top: -80px;
}
footer,
nav,
header {
background: #000;
}
header div,
footer div {
padding: 15px;
}
nav ul {
list-style-type: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.5.1/vuex.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="app">
</div>
<script type="text/x-template" id="header_tpl">
<header class="header">
<div>
header {{ title }}
</div>
</header>
</script>
<script type="text/x-template" id="navbar_tpl">
<nav class="navbar">
<ul>
<li>navbar {{ title }}</li>
</ul>
</nav>
</script>
<script type="text/x-template" id="body_tpl">
<div class="container">
<div>
body {{ title }}
</div>
</div>
</script>
<script type="text/x-template" id="footer_tpl">
<footer class="footer">
<div>
footer {{ title }}
</div>
</footer>
</script>
</body>
</html>
You seem confused about Vue Instance and Vue Component. Basically you only need just one Vue instance with multiple components to create your app.
To answer your first question, it does not work because you didn't install the store to each Vue instance that you are created (you only install just 1 instance called app).
for (var companent_name in app.$root.$options.components) {
if (typeof app.$root.$options.components[companent_name] === 'function') {
var MyComponent = Vue.extend(app.$root.$options.components[companent_name]);
var component = new MyComponent({
store // <-- install here
}).$mount();
document.getElementById('app').appendChild(component.$el);
}
}
Working example here
Actually you can just use store since all store and app.$store and this.$store is the same object. The advantage of this.$store is you have no need to import store to every component file for Single File Components.
To answer your second question,
You are mixing about Global Registration and Local Registration. You should use only one for a component.
For render components you can define your template inside <div id="app"> just like:
<div id="app">
<componentheader></componentheader>
<componentnavbar></componentnavbar>
<componentbody></componentbody>
<componentfooter></componentfooter>
</div>
Working example here

Vue v-for: iterate one element individually in an array

I'm looking to loop through an array of span tags and add is-active to the next one in line, every 3 seconds. I have it working but after the first one, it adds all the rest. How do I just pull that class from the active one and add it to the next array item?
I've read through the official documentation several times and there doesn't seem to be any mention of iterating individual items, just listing them all or pushing an item onto the list.
I'm not sure if 'index' comes in to play here, and how to grab the index of the span element to add/subtract is-active. what am I doing wrong?
var firstComponent = Vue.component('spans-show', {
template: `
<h1>
<span class="unset">Make</span>
<br>
<span class="unset">Something</span>
<br>
<span v-for="(span, index) of spans" :class="{ 'is-active': span.isActive, 'red': span.isRed, 'first': span.isFirst }" :key="index">{{ index }}: {{ span.name }}</span>
</h1>
`,
data() {
return {
spans: [
{
name: 'Magical.',
isActive: true,
isRed: true,
isFirst: true
},
{
name: 'Inspiring.',
isActive: false,
isRed: true,
isFirst: true
},
{
name: 'Awesome.',
isActive: false,
isRed: true,
isFirst: true
}
]
};
},
methods: {
showMe: function() {
setInterval(() => {
// forEach
this.spans.forEach(el => {
if (el.isActive) {
el.isActive = false;
} else {
el.isActive = true;
}
});
}, 3000);
}
},
created() {
window.addEventListener('load', this.showMe);
},
destroyed() {
window.removeEventListener('load', this.showMe);
}
});
var secondComponent = Vue.component('span-show', {
template: `
<span v-show="isActive"><slot></slot></span>
`,
props: {
name: {
required: true
}
},
data() {
return {
isActive: false
};
}
});
new Vue({
el: "#app",
components: {
"first-component": firstComponent,
"second-component": secondComponent
}
});
.container {
position: relative;
overflow: hidden;
width: 100%;
}
.wrapper {
position: relative;
margin: 0 auto;
width: 100%;
padding: 0 40px;
}
h1 {
font-size: 48px;
line-height: 105%;
color: #4c2c72;
letter-spacing: 0.06em;
text-transform: uppercase;
font-family: archia-semibold, serif;
font-weight: 400;
margin: 0;
height: 230px;
}
span {
position: absolute;
clip: rect(0, 0, 300px, 0);
}
span.unset {
clip: unset;
}
span.red {
color: #e43f6f;
}
span.is-active {
clip: rect(0, 900px, 300px, -300px);
}
<div id="app">
<div class="container">
<div class="wrapper">
<spans-show>
<span-show></span-show>
</spans-show>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
To achieve desired result, I'd suggest to change the approach a bit.
Instead of changing value of isActive for individual items, we can create a variable (e.g. activeSpan, that will be responsible for current active span and increment it over time.
setInterval(() => {
// Increment next active span, or reset if it is the one
if (this.activeSpan === this.spans.length - 1) {
this.activeSpan = 0
} else {
this.activeSpan++
}
}, 3000);
In component's template, we make class is-active conditional and dependent on activeSpan variable:
:class="{ 'is-active': index === activeSpan, 'red': span.isRed, 'first': span.isFirst }"
If you still need to update values inside spans array, it can be done in more simple way, via map for example. Also included such case as optional in solution below.
Working example:
JSFiddle
Sidenote: there is no need to add window listeners for load event, as application itself is loaded after DOM is ready. Instead, method can be invoked inside created hook. It is included in solution above.

GraphQL query callbacks for Gatsby.js

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.

CSS styles not being applied to HTML within a Vue Component

I am trying to create a rotating text animation using Vue.js and I used this CodePen as inspiration.
I got all the HMTL elements properly in place (i.e., as in the CodePen mentioned). In short:
each word is formed of several <span> elements, each containing one letter.
following a specific time interval, each <span> that holds a letter gets applied an .in and .out CSS class. This goes on indefinitely.
here is what it looks like in the DOM:
the problem is that no matter what CSS selectors I use, I can't target the .in and .out classes, unless I do it via Developer Tools in Chrome:
original output:
output after I added the classes in Developer Tools:
Here is the bare minimum code of my Vue Component:
<template>
<div id="app-loading">
<div class="words">
<span v-for="setting in settings" v-html="setting.lettersHTML" :id="setting.id" class="word"></span>
</div>
</div>
</template>
<script>
export default {
data() {
return {
settings: [
{ word: 'WordOne', id: 1, lettersArray: null, lettersHTML: null },
{ word: 'WordTwo', id: 2, lettersArray: null, lettersHTML: null }
],
currentWord: 1
}
},
created() {
this.splitLetters();
},
mounted() {
setInterval(this.changeWord, 1500);
},
methods: {
splitLetters() {
this.settings.forEach((setting) => {
let letters = [];
for (let i = 0; i < setting.word.length; i++) {
let letter = `<span class="letter">${ setting.word.charAt(i) }</span>`;
letters.push(letter);
}
setting.lettersArray = letters;
setting.lettersHTML = letters.join('');
});
},
changeWord() {
let current = document.getElementById(this.currentWord).getElementsByTagName('span');
let next = (this.currentWord == this.settings.length) ? document.getElementById(1).getElementsByTagName('span') : document.getElementById(this.currentWord + 1).getElementsByTagName('span');
// Animate the letters in the current word.
for (let i = 0; i < current.length; i++) {
this.animateLetterOut(current, i);
}
// Animate the letters in the next word.
for (let i = 0; i < next.length; i++) {
this.animateLetterIn(next, i);
}
this.currentWord = (this.currentWord == this.settings.length) ? 1 : this.currentWord + 1;
},
animateLetterOut(current, index) {
setTimeout(() => {
current[index].className = 'letter out';
}, index * 300);
},
animateLetterIn(next, index) {
setTimeout(() => {
next[index].className = 'letter in';
}, 340 + (index * 300));
}
}
}
</script>
<style lang="scss" scoped>
#app-loading {
font-size: 4rem;
}
.words, .word {
border: 1px solid rosybrown;
}
.letter {
text-decoration: underline; // Not working.
}
.letter.in {
color: red; // Not working.
}
.letter.out {
color: blue; // Not working.
}
</style>
What goes wrong that prevents these classes from being applied?
You're using v-html, but that doesn't work with scoped styles.
DOM content created with v-html are not affected by scoped styles, but you can still style them using deep selectors.
This worked for me:
<template>
<div class="a" v-html="content"></div>
</template>
<script>
export default {
data() {
return {
content: 'this is a <a class="b">Test</a>',
}
},
}
</script>
<style scoped>
.a ::v-deep .b {
color: red;
}
</style>
Yes,
v-html
doesn't work with scoped styles.
As Brock Reece explained in his article Scoped Styles with v-html, it should be solved like this:
<template>
<div class="a" v-html="content"></div>
</template>
<script>
export default {
data() {
return {
content: 'this is a <a class="b">Test</a>',
}
},
}
</script>
<style scoped>
.a >>> .b {
color: red;
}
</style>
Most answers are deprecated now that Vue3 is out.
Up-to-date usage of deep selector:
.letter{
&:deep(.in) {
color:blue;
}
&:deep(.out) {
color:red;
}
}
Vue3: In Single-File Components, scoped styles will not apply to content inside v-html, because that HTML is not processed by Vue's template compiler.
You can use :deep() inner-selector in Vue3 project.
Here is a example:
<script setup lang="ts">
import {onMounted,ref } from 'vue'
const content = ref("")
onMounted(()=>{
window.addEventListener('keydown',event =>{
content.value = `
<div class="key">
<span class="content">${event.keyCode}</span>
<small>event.keyCode</small>
</div>
`
})
})
</script>
<template>
<div class="container" v-html="content">
</div>
</template>
<style lang="scss" scoped>
.container{
display: flex;
:deep(.key){
font-weight: bold;
.content{
font-size: 1.5rem;
}
small{
font-size: 14px;
}
}
}
</style>

Categories

Resources