Pro Yearly is on sale from $80 to $50! »

Structured content & JS Components – a match made in heaven

22725c2d3eb331146549bf0d5d3c050c?s=47 stefan judis
September 27, 2017

Structured content & JS Components – a match made in heaven

22725c2d3eb331146549bf0d5d3c050c?s=128

stefan judis

September 27, 2017
Tweet

Transcript

  1. Structured content and JS Components @stefanjudis A match made in

    heaven
  2. Stefan Judis Frontend Developer, Occasional Teacher, Meetup Organizer ❤ Open

    Source, Performance and Accessibility ❤ @stefanjudis
  3. None
  4. In ❤ with Web Technology for seven years

  5. None
  6. Brad Frost

  7. Atomic Design

  8. Atoms

  9. Atoms

  10. Molecules

  11. Molecules

  12. Organisms

  13. Organisms

  14. Templates

  15. Templates

  16. Building blocks in CSS

  17. Frontend Engineers needed more

  18. Component-driven JavaScript Frameworks

  19. Component-driven JavaScript Frameworks <Router onChange={this.handleRoute}> <Home path="/" /> <Preview path="/preview/:id"

    /> </Router> <Transition in={inProp} timeout={duration}> ... </Transition> <Anything can="be" a="component"> </Anything>
  20. Component-driven JavaScript Frameworks

  21. Component-driven JavaScript Frameworks Virtual DOM implementation

  22. vdom What is this virtual DOM?

  23. - Virtual DOM in a nutshell - /** @jsx h

    */ const meetup = ( <div> <h1 id="headline">CTF Meetup</h1> <p>Hello! "</p> </div> );
  24. - Virtual DOM in a nutshell - /** @jsx h

    */ const meetup = ( <div> <h1 id="headline">CTF Meetup</h1> <p>Hello! "</p> </div> ); const meetup = h( "div", null, h( "h1", { id: "headline" }, "CTF Meetup" ), h( "p", null, "Hello! "" ) );
  25. - Virtual DOM in a nutshell - /** @jsx h

    */ const meetup = ( <div> <h1 id="headline">CTF Meetup</h1> <p>Hello! "</p> </div> ); const meetup = h( "div", null, h( "h1", { id: "headline" }, "CTF Meetup" ), h( "p", null, "Hello! "" ) );
  26. - Virtual DOM in a nutshell - /** @jsx h

    */ const meetup = ( <div> <h1 id="headline">CTF Meetup</h1> <p>Hello! "</p> </div> ); const meetup = h( "div", null, h( "h1", { id: "headline" }, "CTF Meetup" ), h( "p", null, "Hello! "" ) ); NodeName NodeName NodeName
  27. - Virtual DOM in a nutshell - /** @jsx h

    */ const meetup = ( <div> <h1 id="headline">CTF Meetup</h1> <p>Hello! "</p> </div> ); const meetup = h( "div", null, h( "h1", { id: "headline" }, "CTF Meetup" ), h( "p", null, "Hello! "" ) ); NodeName NodeName NodeName Attributes Attributes Attributes
  28. - Virtual DOM in a nutshell - /** @jsx h

    */ const meetup = ( <div> <h1 id="headline">CTF Meetup</h1> <p>Hello! "</p> </div> ); const meetup = h( "div", null, h( "h1", { id: "headline" }, "CTF Meetup" ), h( "p", null, "Hello! "" ) ); NodeName NodeName NodeName Attributes Attributes Attributes Children Children Children Children
  29. export function h(nodeName, attributes) { let children=EMPTY_CHILDREN, lastSimple, child, simple,

    i; for (i=arguments.length; i-- > 2; ) { stack.push(arguments[i]); } if (attributes && attributes.children!=null) { if (!stack.length) stack.push(attributes.children); delete attributes.children; } while (stack.length) { if ((child = stack.pop()) && child.pop!==undefined) { for (i=child.length; i--; ) stack.push(child[i]); } else { if (typeof child==='boolean') child = null; if ((simple = typeof nodeName!=='function')) { if (child==null) child = ''; - Virtual DOM in a nutshell - github.com/developit/preact/blob/master/src/h.js
  30. children = [child]; } else { children.push(child); } lastSimple =

    simple; } } let p = new VNode(); p.nodeName = nodeName; p.children = children; p.attributes = attributes==null ? undefined : attributes; p.key = attributes==null ? undefined : attributes.key; // if a "vnode hook" is defined, pass every created VNode to it if (options.vnode!==undefined) options.vnode(p); return p; } - Virtual DOM in a nutshell - github.com/developit/preact/blob/master/src/h.js
  31. children = [child]; } else { children.push(child); } lastSimple =

    simple; } } let p = new VNode(); p.nodeName = nodeName; p.children = children; p.attributes = attributes==null ? undefined : attributes; p.key = attributes==null ? undefined : attributes.key; // if a "vnode hook" is defined, pass every created VNode to it if (options.vnode!==undefined) options.vnode(p); return p; } - Virtual DOM in a nutshell - github.com/developit/preact/blob/master/src/h.js
  32. - Virtual DOM in a nutshell - const meetup =

    h( "div", null, h( "h1", { id: "headline" }, "CTF Meetup" ), h( "p", null, "Hello! "" ) );
  33. - Virtual DOM in a nutshell - const meetup =

    h( "div", null, h( "h1", { id: "headline" }, "CTF Meetup" ), h( "p", null, "Hello! "" ) ); { "nodeName": "div", "children": [ { "nodeName": "h1", "children": [ "CTF Meetup" ], "attributes": { "id": "headline" } }, { "nodeName": "p", "children": [ "Hello! "" ] } ] }
  34. - Virtual DOM in a nutshell - function render(vnode) {

    // Strings just convert to #text Nodes: if (vnode.split) return document.createTextNode(vnode); // create a DOM element with the nodeName of our VDOM element: let n = document.createElement(vnode.nodeName); // copy attributes onto the new node: let a = vnode.attributes || {}; Object.keys(a).forEach( k => n.setAttribute(k, a[k]) ); // render (build) and then append child nodes: (vnode.children || []).forEach( c => n.appendChild(render(c)) ); return n; }
  35. It's "just" data!

  36. None
  37. { "nodeName": "Page", "children": [ { "nodeName": "h1", "children": [

    "Landing Page" ] }, { "nodeName": "header", "children": [{ "nodeName": "HeroImage", ... }] }, { "nodeName": "main", "children": [{ "nodeName": "Article", ... }] }, ... ] }
  38. WTF is JSX The benefit of virtual DOM is that

    it is
 extremely lightweight. Small objects referring to other small objects,
 a structure composed by easily optimizable application logic. This also means it is not tied 
 to any rendering logic or slow DOM methods. “
  39. The benefit of virtual DOM is that it is
 extremely

    lightweight. Small objects referring to other small objects, a structure composed by easily optimizable application logic. This also means it is not tied 
 to any rendering logic or slow DOM methods. “ WTF is JSX
  40. It's "just" data! This got me thinking...

  41. What if we could connect content types and components?

  42. None
  43. None
  44. None
  45. None
  46. None
  47. { "sys": { "contentType": { "sys": { "id": "page" }

    } }, "fields": { "title": "Landing Page", "header": [ { "sys": { "contentType": { "sys": { "id": "heroImage" } } } } ] }, ... }
  48. { "sys": { "contentType": { "sys": { "id": "page" }

    } }, "fields": { "title": "Landing Page", "header": [ { "sys": { "contentType": { "sys": { "id": "heroImage" } } } } ] }, ... } ? Contentful response vDOM { "nodeName": "Page", "children": [ { "nodeName": "h1", "children": [ "Landing Page" ] }, { "nodeName": "header", "children": [{ "nodeName": "HeroImage", ... }] }, { "nodeName": "main", "children": [{ "nodeName": "Article", ... }] }, ... ] }
  49. { "sys": { "contentType": { "sys": { "id": "page" }

    } }, "fields": { "title": "Landing Page", "header": [ { "sys": { "contentType": { "sys": { "id": "heroImage" } } } } ] }, ... } ? Contentful response vDOM { "nodeName": "Page", "children": [ { "nodeName": "h1", "children": [ "Landing Page" ] }, { "nodeName": "header", "children": [{ "nodeName": "HeroImage", ... }] }, { "nodeName": "main", "children": [{ "nodeName": "Article", ... }] }, ... ] }
  50. export default class Home extends Component { componentWillMount() { createClient({

    space: '...', accessToken: '...' }).getEntries({ 'sys.id': '1PYzQGJdgEG2Ycqweqqmk0' }).then(response => { this.setState({ page: response.items[0] }); }); } render(props, state) { return ( <section> { state.page && ( <CtfModule entry={state.page} /> ) } </section> ); } } - Glue Components and Content Types together - routes/home/index.js
  51. export default class Home extends Component { componentWillMount() { createClient({

    space: '...', accessToken: '...' }).getEntries({ 'sys.id': '1PYzQGJdgEG2Ycqweqqmk0' }).then(response => { this.setState({ page: response.items[0] }); }); } render(props, state) { return ( <section> { state.page && ( <CtfModule entry={state.page} /> ) } </section> ); } } - Glue Components and Content Types together - routes/home/index.js
  52. export default class Home extends Component { componentWillMount() { createClient({

    space: '...', accessToken: '...' }).getEntries({ 'sys.id': '1PYzQGJdgEG2Ycqweqqmk0' }).then(response => { this.setState({ page: response.items[0] }); }); } render(props, state) { return ( <section> { state.page && ( <CtfModule entry={state.page} /> ) } </section> ); } } - Glue Components and Content Types together - routes/home/index.js
  53. export default class Home extends Component { componentWillMount() { createClient({

    space: '...', accessToken: '...' }).getEntries({ 'sys.id': '1PYzQGJdgEG2Ycqweqqmk0' }).then(response => { this.setState({ page: response.items[0] }); }); } render(props, state) { return ( <section> { state.page && ( <CtfModule entry={state.page} /> ) } </section> ); } } - Glue Components and Content Types together - routes/home/index.js
  54. export default function CtfModule({ children, ...props }) { if (!props.entry)

    { return ''; } const entries = props.entry instanceof Array ? props.entry : [ props.entry ]; return h( 'div', { class: 'ctf-modules' }, [ entries.map( entry => h( ContentfulComponents[getComponentName(entry.sys.contentType.sys.id)], { fields: entry.fields } ) ) ] ); } - Glue Components and Content Types together - components/contentful-module/index.js
  55. export default function CtfModule({ children, ...props }) { if (!props.entry)

    { return ''; } const entries = props.entry instanceof Array ? props.entry : [ props.entry ]; return h( 'div', { class: 'ctf-modules' }, [ entries.map( entry => h( ContentfulComponents[getComponentName(entry.sys.contentType.sys.id)], { fields: entry.fields } ) ) ] ); } - Glue Components and Content Types together - components/contentful-module/index.js
  56. export default function CtfModule({ children, ...props }) { if (!props.entry)

    { return ''; } const entries = props.entry instanceof Array ? props.entry : [ props.entry ]; return h( 'div', { class: 'ctf-modules' }, [ entries.map( entry => h( ContentfulComponents[getComponentName(entry.sys.contentType.sys.id)], { fields: entry.fields } ) ) ] ); } - Glue Components and Content Types together - components/contentful-module/index.js
  57. export default function CtfModule({ children, ...props }) { if (!props.entry)

    { return ''; } const entries = props.entry instanceof Array ? props.entry : [ props.entry ]; return h( 'div', { class: 'ctf-modules' }, [ entries.map( entry => h( ContentfulComponents[getComponentName(entry.sys.contentType.sys.id)], { fields: entry.fields } ) ) ] ); } - Glue Components and Content Types together - components/contentful-module/index.js
  58. export default function CtfModule({ children, ...props }) { if (!props.entry)

    { return ''; } const entries = props.entry instanceof Array ? props.entry : [ props.entry ]; return h( 'div', { class: 'ctf-modules' }, [ entries.map( entry => h( ContentfulComponents[getComponentName(entry.sys.contentType.sys.id)], { fields: entry.fields } ) ) ] ); } - Glue Components and Content Types together - components/contentful-module/index.js entry => h( HeroImage, { fields: entry.fields } ) entry => h( Article, { fields: entry.fields } ) entry => h( Person, { fields: entry.fields } ) content type dependent components
  59. export default function Page({ children, ...props }) { return (

    <div> { props.fields.header && ( <header class={style.header}> <span>Header Area</span> <CtfModule entry={props.fields.header} /> </header> )} { props.fields.main && ( <main class={style.main}> <span>Main Area</span> <CtfModule entry={props.fields.main} /> </main> )} { props.fields.header && ( <footer class={style.footer}> <span>Footer Area</span> <CtfModule entry={props.fields.footer} /> </footer> )} </div> ); } - Glue Components and Content Types together - components/page/index.js
  60. export default function Page({ children, ...props }) { return (

    <div> { props.fields.header && ( <header class={style.header}> <span>Header Area</span> <CtfModule entry={props.fields.header} /> </header> )} { props.fields.main && ( <main class={style.main}> <span>Main Area</span> <CtfModule entry={props.fields.main} /> </main> )} { props.fields.header && ( <footer class={style.footer}> <span>Footer Area</span> <CtfModule entry={props.fields.footer} /> </footer> )} </div> ); } - Glue Components and Content Types together - components/page/index.js
  61. export default function Page({ children, ...props }) { return (

    <div> { props.fields.header && ( <header class={style.header}> <span>Header Area</span> <CtfModule entry={props.fields.header} /> </header> )} { props.fields.main && ( <main class={style.main}> <span>Main Area</span> <CtfModule entry={props.fields.main} /> </main> )} { props.fields.header && ( <footer class={style.footer}> <span>Footer Area</span> <CtfModule entry={props.fields.footer} /> </footer> )} </div> ); } - Glue Components and Content Types together - components/page/index.js
  62. - Glue Components and Content Types together - components/hero-image/index.js components/article/index.js

    components/person/index.js export default function HeroImage({ children, ...props }) { return ( <header class={style.container}> <h1 class={style.headline}>{props.fields.headline}</h1> <img src={props.fields.image.fields.file.url} /> </header> ); } export default function Article({ children, ...props }) { return ( <header> <h2>{props.fields.title}</h2> <main>{props.fields.main}</main> </header> ); } export default function Person({ children, ...props }) { return ( <header class={style.person}> <h3>{props.fields.name}</h3> <img src={props.fields.image.fields.file.url + '?w=200'} /> </header> ); } HeroImage Article Person
  63. Header Page Main Footer Article HeroImage Person

  64. Header Page Main Person Footer Article HeroImage Person

  65. Header Person Page Main Footer Article HeroImage Person

  66. Header Page Main Footer HeroImage Person

  67. Previewing content can be hard...

  68. export default class Home extends Component { componentWillMount() { createClient({

    space: '...', accessToken: '...' }).getEntries({ 'sys.id': '1PYzQGJdgEG2Ycqweqqmk0' }).then(response => { this.setState({ page: response.items[0] }); }); } render(props, state) { return ( <section> { state.page && ( <CtfModule entry={state.page} /> ) } </section> ); } } - Glue Components and Content Types together - routes/home/index.js
  69. export default class Home extends Component { componentWillMount() { createClient({

    space: '...', accessToken: '...' }).getEntries({ 'sys.id': this.props.matches.id }).then(response => { this.setState({ entry: response.items[0] }); }); } render(props, state) { return ( <section> { state.page && ( <h2>Component preview</h2> <CtfModule entry={state.entry} /> ) } </section> ); } } - Glue Components and Content Types together - routes/preview/index.js
  70. export default class Home extends Component { componentWillMount() { createClient({

    space: '...', accessToken: '...' }).getEntries({ 'sys.id': this.props.matches.id }).then(response => { this.setState({ entry: response.items[0] }); }); } render(props, state) { return ( <section> { state.page && ( <h2>Component preview</h2> <CtfModule entry={state.entry} /> ) } </section> ); } } CtfModule renders the particular components automatically. - Glue Components and Content Types together - routes/preview/index.js
  71. export default class Home extends Component { componentWillMount() { createClient({

    space: '...', accessToken: '...' }).getEntries({ 'sys.id': this.props.matches.id }).then(response => { this.setState({ entry: response.items[0] }); }); } render(props, state) { return ( <section> { state.page && ( <h2>Component preview</h2> <CtfModule entry={state.entry} /> ) } </section> ); } } CtfModule renders the particular components automatically. - Glue Components and Content Types together - routes/preview/index.js The components render data automatically.
  72. None
  73. None
  74. It's "just" data! This got me thinking...

  75. <CtfModule /> - New workflows? -

  76. - New workflows? - "Contentful UI components"?

  77. - New workflows? -

  78. - New workflows? -

  79. - New workflows? -

  80. So far it's just an idea in my head...

  81. Structured Data JS Components

  82. Thanks. @stefanjudis Slides ctfl.io/js-components