Nova
Introduction
Quick Start

Quick Start Guide

This page will introduce you to the majority of Nova concepts you'll use daily.

️⚠️

This page contains accurate information but needs some proofing and editing.

Prerequisites

Topics include

  • Creating and using components
  • Adding markup and styles
  • Working with mock and WordPress data
  • Customizing brand configurations

How does it work?

By technology

  • We use React to write our markup and SCSS to style components in the component library.
  • The React code we write is transpiled from JSX to PHP templates.
  • When WordPress loads, we determine which template we're using and pass relevant context to Nova (post ID, author slug, category name, etc.)
  • Nova takes that information and passes it to the template model, which recursively creates an object representing all of the data needed by the template.
  • Default props are merged based on the component schema before being passed into the template PHP,
  • That data is passed into our templates, rendering the fully-formed markup and styles.

By WordPress

  • WordPress loads, running the request through the rewrite rules.
  • The main WP_Query runs and globals are set.
  • Nova takes over when the template_redirect action fires.
  • Nova's component loader determines which Nova component template maps to the results of the WP_Query.
  • Relevant contextual data (post_id, object slugs, etc) is passed into the model for the template component.
  • Using the request's contextual data, the model builds an object containing the data needed to render the template component.
  • If everything is valid, Nova passes the data into the component's PHP template via the component's controller.
  • The template renders and WordPress returns the request output.

Creating and using components

Throughout this guide, we'll refer to a Nova component called Example Component.

If you want to follow along, you can create this example using

npm run nova scaffold component example-component

Which will create a file in pmc-nova/style-guide/components/example-component/.

Basic example

Nova's component library and templating begins with a React component.

Here's our example-component/index.js,

const ExampleComponent = ( { text = '' } ) => {
	return (
		<div data-component="example-component">
			<p>{ text }</p>
		</div>
	);
}
️👍

All components require a data-component attribute; it's useful for debugging and as a CSS or JavaScript selector.

Using another component

We can use other components in the Component Library just like standard React.

Here's an example replacing our text string, with the <Text /> component.

// Components.
import Text from '../text';
 
/**
 * Nova Component component.
 *
 * @param  {null|Object} options.headingText  Text component props.
 */
const ExampleComponent = ( {
	alias = '',
	headingText = null,
} ) => {
	return (
		<div data-component="example-component">
			{ null !== headingText && <Text { ...headingText } /> }
		</div>
	);
}
️👍

You can import any component from the library. Unlike Larva, everything is a component in Nova, including the templates (ref).

In this example we're,

  1. Importing the <Text /> component.
  2. Adding a headingText prop, which is either null or an Object.

However, we also need to update the component schema so our data and defaults are able to contextualize the use of <Text />.

So inside of our example-component/schema.json we need a new key for headingText,

"headingText": {
	"oneOf": [
		{ "type": "null" },
		{ "$ref": "https://nova.pmcdev.io/schemas/text/schema.json" }
	]
},

Transpiler

Refer to the React documentation (opens in a new tab).

During the build process Nova transpiles the React JSX into PHP. This results in the template.php file in each component.

Learn more about the transpiler

️👍

Understanding what gets compiled and doesn't will unlock a lot of clarity on Nova's inner workings.

The transpiler targets the final return in our components. It does not actually transpile the entire component or props, which is why the schema is required.

This is very nuanced and additional reading is recommended.

Data attributes

Nova heavily uses data attributes as CSS selectors.

  • Every component has a data-component attribute.
  • Using attributes such as data-theme, data-size, or data-layout allow us to easily contextualize CSS and improve the debugging process.
  • Adding a data-alias attribute to any HTML element allows us to easily target any element with CSS. Use BEM naming conventions, i.e. data-alias="example-component__wrapper".

Adding styles

Keeping CSS organized, consistent, and intentional is critical to the long-term health of Nova.

️👍

We follow the patterns and ideas in Every Layout to achieve this. It provides theoretical and practical guidance for writing and maintaining styles.

Keep Reading: Every Layout

SCSS

Nova uses SCSS for styling (opens in a new tab).

If you're not familiar, it's basically CSS with some extra functionality on top.

// Example.
@use './../../mixins/_helpers.scss';
 
/**
 * Example Component component.
 */
.exampleComponent {
	background: red;
 
	.inner {
		background: blue;
 
		&:hover {
			text-decoration: none;
 
			@include helpers.applyBorders();
		}
	}
}

Layout primitives

Nova offers Every Layout's layout primitives as mixins (opens in a new tab).

Nova uses layout primitives almost exclusively to implement component layouts and overall structure. These provide simple and responsive layouts which can be mixed and matched easily to build nearly any required component.

@use './../../../mixins/_layout-primitives.scss';
 
.exampleComponent {
	@include layout-primitives.sidebar(
		$sidebar-width: 150px,
		$side: right,
	);
}

What am I styling?

When styling something in Nova you'll need to consider two questions,

  1. Should my styles be applied to all brands, or is to brand-specific?
  2. Are my styles localized to a component, or applied globally?

Global Styles / All Brands

  • CSS resets.
  • Global variables for breakpoint widths, typography, spacing, and z-indexes.
  • Global styles based on dynamic properties such as ads or the admin bar.

Nova Core styles (opens in a new tab).

Global Styles / Single Brand

  • Brand tokens.
  • CSS variables for borders and other accents.
  • Font loading (until automated).

It is primarily used for tokens, although any styles can be added here.

See the ${brand-name}.scss (Nova ref (opens in a new tab)) files in the brands directory (opens in a new tab).

Component Styles / All Brands

Responsible for the Layout layer (docs).

Found at styles.module.scss in every component.

Component Styles / Single Brand

Responsible for the Brand layer (docs).

Found at /example-component/brands/${brand-name}.module.scss in every component is applied to all brands.

Note: We don't scaffold a brand file for every component, run npm run build after creating it.

When you're ready to start writing styles, use SCSS (opens in a new tab) and CSS Modules (opens in a new tab) (for components).

Global Styles / Single Brand

Every brand has a body class that's required to keep global brand styles separate,

/**
 * Typography for IndieWire.
 */
body.pmc-nova--indiewire {
	background: blue;
}
️😔

NextJS can't conditionally import these files, so all brands load at once on the style guide. This class prevents collisions.

Component Styles

Component styles use CSS modules to namespace classes,

/**
 * Nova layout styling for Ad Unit.
 *
 * Used by all brands.
 */
.adUnit {
	background: blue;
}

Data

Every component has,

  • A __mocks__/data.js file containing mock data for the Style Guide.
  • A class-model.php file responsible for implementing WordPress data.

Mocking data

The data and default exports in __mocks__/data.js will contain mock data.

  • data is an object containing an array of mock object, mapped to each brand.
  • default has an array of mock objects.

The mock data should contain values that would otherwise come from WordPress. Default values or brand settings should be set the component defaults.

// Helpers.
import postsDataProvider from '../../../helpers/data/posts-data-provider';

export const data = postsDataProvider( () => {
	return [
		{
			headingText: {
				text: 'Example One',
			},
		},
		{
			headingText: {
				text: 'Example Two',
			},
		},
		{
			headingText: {
				text: 'Example Three',
			},
		},
	];
} );

export default data.nova;

These values will be merged with component defaults and other values, finally accessible as props.managedData in any documenation.mdx file.

<ExampleComponent { ...props.managedData } />

Displaying WordPress data

Inside of the class-model.php find a class implementing _populate_data(). This method is what takes values passed from a parent component (or endpoint for template components), and hydrates the data.

/**
 * Class Model.
 */
final class Model extends Base {
 
	/**
	 * The text to display.
	 *
	 * @var string
	 */
	public string $text;
 
	/**
	 * Populate data structure.
	 */
	protected function _populate_data(): void {
		$this->_data->text = $text;
	}
}

Hydrating children components

Use the _add_nested_model_data() method to easily hydrate and get a child component's data.

In this case, we're hydrating a <Text /> component used by our parent as headingText.

$this->_add_nested_model_data(
	'headingText',
	Components\Text\Model::class,
	[
		'text'  => 'Hello World',
		'theme' => 'primary-xl'
	]
);

Error handling

If a component isn't passed valid or required values from a parent, it can set the _return_no_data flag to indicate to the parent that hydration failed and it should be treated as a null value.

// Return early because text is required.
if ( empty( $this->text ) ) {
	$this->_return_no_data = true;
	return;
}

Component defaults

Similar to styles, every component also has a file of "Nova Core defaults" and any number of "Brand defaults" files.

These values are automatically applied to the mock data and WordPress model data. This provides a clean syntax for setting defaults and customizing properties for brands at the component level.

  • The defaults.json file contains Nova Core defaults.
  • The brands/${brand-name}.json file contains brand defaults.
    • Same as the component brand styles (${brand-name}.module.scss), we don't scaffold a file for each brand automatically. Create these as needed and run a build.

These JSON files contain various settings and configurations for the component. Think of them as little switches that brands can override from Nova.

Our example component uses a <Text /> component at the property headingText, which means we can set any prop on the <Text /> schema, such as theme to control the typography, transform to transform the text with CSS, and truncate to limit the text displayed.

{
	"headingText": {
		"theme": "secondary-xs",
		"transform": "uppercase",
		"truncate": 3
	}
}

Merge strategy - Nova Core and Brands

The values in the brand defaults will take precedence over Nova Core.

// defaults.json
{
	"theme": "default"
}
// brands/${brand-name}.json
{
	"theme": "primary"
}

Results in "theme": "primary".

Merge strategy - Parent and Children

Values set in a parent component will take precedence over the child's defaults.

// text/defaults.json
{
	"theme": "primary"
}
// example-component/defaults.json
{
	"headingText": {
		"theme": "secondary"
	}
}

Results in "theme": "secondary" because the module's usage overrides the text component.

null values for components

In Nova, we use null to indicate an invalid component. When used with defaults, this allows us to easily toggle components on/off for brands.

// example-component/defaults.json
{
	"headingText": null
}

Or we can enable it by setting an empty object for a brand.

// example-component/brands/${brand-name}.json
{
	"headingText": {}
}

Writing Frontend JavaScript

WordPress frontend JavaScript is located in the /src/js directory (ref (opens in a new tab)).

️✏️

While you may see the occasional setState() in the React code for mocking purposes, interactive elements on the WordPress frontend require their own JavaScript.

Create the file

Create a new file in /src/js/example-component.js with your JavaScript.

/**
 * Example script that waits for the DOM to load and loops through every
 * `<ExampleComponent />`.
 */
document.addEventListener( 'DOMContentLoaded', () => {
 
	// Convert nodeList to array to more easily loop.
	const exampleComponents = [
		...document.querySelectorAll( '[data-component="example-component"]' ),
	];
 
	// Do something with each <ExampleComponent />.
	exampleComponents.forEach( ( exampleComponent ) => {} );
} );

Run the build

The build:wp-assets command will automatically create built files and a dependency array in /wp-assets/.

This will create two files,

  1. The built JavaScript, /wp-assets/example-component.js
  2. Asset version and dependency information in /wp-assets/example-component.asset.php

Enqueue assets

The assets can now be loaded in WordPress using the core wp_enqueue_scripts action and our PMC_Scripts::enqueue_asset() utility.

Since our example script is directly associated with the example-component, the example-component/class-controller.php is the best place to enqueue. This ensures that the script is only enqueued when a component is used, avoiding unused JS on the frontend.

The controller's base class provides a _register_hooks() method to assist with this.

/**
 * Class Controller.
 */
final class Controller extends Base {
	/**
	 * Register component-specific hooks.
	 *
	 * Typically, this is used to render output from a dependent plugin or other
	 * source outside of Nova.
	 *
	 * @return void
	 */
	protected function _register_hooks(): void {
		add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_script' ] );
	}
 
	/**
	 * Enqueue the component's frontend JavaScript.
	 *
	 * @return void
	 */
	public function enqueue_script(): void {
		PMC_Scripts::enqueue_asset(
			'nova-example-component',
			'example-component',
			WP_ASSETS_PATH,
			[],
			true
		);
	}
}

You should now see your JavaScript whenever your assets are enqueued. In this example, it only enqueues when

Keep reading: Frontend JavaScript.

What's next