Skip to content
This repository has been archived by the owner on Oct 23, 2023. It is now read-only.

Commit

Permalink
feat(preview): update preview when pattern code changes (fixes #27)
Browse files Browse the repository at this point in the history
  • Loading branch information
TheReincarnator authored and lkuechler committed Apr 6, 2018
1 parent 3698d7d commit f30d832
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 54 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"@commitlint/travis-cli": "^6.0.2",
"@patternplate/cli": "2",
"@patternplate/render-styled-components": "2",
"@types/chokidar": "^1.7.5",
"@types/deep-assign": "^0.1.1",
"@types/electron-devtools-installer": "^2.0.2",
"@types/fs-extra": "^5.0.1",
Expand Down Expand Up @@ -118,6 +119,7 @@
"typedoc": "^0.11.0"
},
"dependencies": {
"chokidar": "^2.0.3",
"cli": "1.0.1",
"deep-assign": "^2.0.0",
"electron-log": "2.2.14",
Expand Down
1 change: 1 addition & 0 deletions src/electron/renderer/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ ipcRenderer.on('preview-ready', (readyEvent: {}, readyMessage: JsonObject) => {
projects: store.getProjects().map(project => project.toJsonObject()),
styleguidePath: styleguide ? styleguide.getPath() : undefined
};

sendWebViewMessage(message, 'styleguide-change');
});

Expand Down
2 changes: 1 addition & 1 deletion src/electron/renderer/preview.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</head>

<body>
<div id="app">
<div id="preview">
<style>
.outer {
min-height: 100%;
Expand Down
20 changes: 14 additions & 6 deletions src/store/styleguide/styleguide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ export class Styleguide {
}

/**
* Returns the path of the root folder of the designs (projects, pages)
* Returns the absolute and OS-specific path of the root folder of the designs (projects, pages)
* in the currently opened styleguide.
* @return The page root path.
* @return The absolute and OS-specific page root path.
*/
public getPagesPath(): string {
return PathUtils.join(this.path, 'alva');
Expand All @@ -116,7 +116,7 @@ export class Styleguide {
/**
* Returns the absolute and OS-specific path to the styleguide top-level directories.
* This is where the projects, pages, and the pattern implementations are located.
* @return The root path of the styleguide.
* @return The absolute and OS-specific root path of the styleguide.
*/
public getPath(): string {
return this.path;
Expand All @@ -141,9 +141,17 @@ export class Styleguide {
}

/**
* Returns the path of the root folder of the built patterns (like atoms, modules etc.)
* in the currently opened styleguide.
* @return The patterns root path.
* Returns all known (parsed) pattern informations.
* @return All known (parsed) pattern informations.
*/
public getPatterns(): Pattern[] {
return Array.from(this.patterns.values());
}

/**
* Returns the absolute and OS-specific path of the root folder of the built patterns
* (like atoms, modules etc.) in the currently opened styleguide.
* @return The absolute and OS-specific patterns root path.
*/
public getPatternsPath(): string {
return PathUtils.join(this.path, 'lib', 'patterns');
Expand Down
13 changes: 9 additions & 4 deletions src/styleguide/analyzer/styleguide-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ import { Styleguide } from '../../store/styleguide/styleguide';

/**
* A styleguide analyzer walks through the pattern implementations of a styleguide.
* It finds folders and patterns, including multiple files within a folder, and multiple exports within a file.
* It then creates pattern folder and pattern instances representing the implementations, and puts them into the styleguide registry.
* It finds folders and patterns, including multiple files within a folder, and multiple exports
* within a file.
* It then creates pattern folder and pattern instances representing the implementations, and puts
* them into the styleguide registry.
* @see README.md for more details on analyzers and how to write your own.
*/
export abstract class StyleguideAnalyzer {
/**
* Analyzes the pattern implementation directories starting the configured root path, and puts all pattern folders and patterns into the styleguide registry.
* Note: Implementations should call the styleguide's addPattern method, and optionally create new pattern folders based on the styleguide's patternRoot, and also add the pattern to these folders (also addPattern).
* Analyzes the pattern implementation directories starting the configured root path, and puts
* all pattern folders and patterns into the styleguide registry.<br>
* Note: Implementations should call the styleguide's addPattern method, and optionally create
* new pattern folders based on the styleguide's patternRoot, and also add the pattern to these
* folders (also addPattern).
* @param styleguide The styleguide to analyze its implementations.
*/
public abstract analyze(styleguide: Styleguide): void;
Expand Down
10 changes: 10 additions & 0 deletions src/styleguide/renderer/react/pattern-component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as React from 'react';

export interface PatternComponentProps {
patternFactory: React.StatelessComponent | ObjectConstructor;
// tslint:disable-next-line:no-any
patternProps: any;
}

export const PatternComponent: React.StatelessComponent<PatternComponentProps> = props =>
React.createElement(props.patternFactory, props.patternProps);
151 changes: 108 additions & 43 deletions src/styleguide/renderer/react/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { AssetProperty } from '../../../store/styleguide/property/asset-property';
import * as Chokidar from 'chokidar';
import { ErrorMessage } from './error-message';
import { HighlightArea } from '../highlight-area';
import { observable } from 'mobx';
import * as MobX from 'mobx';
import { observer } from 'mobx-react';
import { Page } from '../../../store/page/page';
import { PageElement } from '../../../store/page/page-element';
import * as PathUtils from 'path';
import { Pattern } from '../../../store/styleguide/pattern';
import { PatternComponent } from './pattern-component';
import { Placeholder } from './placeholder';
import { PropertyValue } from '../../../store/page/property-value';
import * as React from 'react';
Expand All @@ -19,6 +22,7 @@ export interface PreviewAppState {

interface PreviewProps {
page?: Page;
patternFactories: Map<string, React.StatelessComponent | ObjectConstructor>;
selectedElementId?: string;
}

Expand Down Expand Up @@ -56,26 +60,23 @@ class PatternWrapper extends React.Component<PatternWrapperProps, PatternWrapper

@observer
class Preview extends React.Component<PreviewProps> {
@observable private highlightArea: HighlightArea;
private patternFactories: { [id: string]: React.StatelessComponent | ObjectConstructor };
private patternWrapperRef: PatternWrapper;
@MobX.observable protected highlightArea: HighlightArea;
protected patternWrapperRef: PatternWrapper;

public constructor(props: PreviewProps) {
super(props);
this.patternFactories = {};

this.highlightArea = new HighlightArea();
}

// tslint:disable-next-line:no-any
private collectChildren(componentProps: any, pageElement: PageElement): void {
protected collectChildren(componentProps: any, pageElement: PageElement): void {
componentProps.children = pageElement
.getChildren()
.map((child, index) => this.createComponent(child));
}

// tslint:disable-next-line:no-any
private collectPropertyValues(componentProps: any, pageElement: PageElement): void {
protected collectPropertyValues(componentProps: any, pageElement: PageElement): void {
const pattern = pageElement.getPattern() as Pattern;
pattern.getProperties().forEach(property => {
const propertyId = property.getId();
Expand All @@ -93,7 +94,7 @@ class Preview extends React.Component<PreviewProps> {
this.triggerHighlight();
}

private createAssetComponent(pageElement: PageElement): JSX.Element {
protected createAssetComponent(pageElement: PageElement): JSX.Element {
const src = pageElement.getPropertyValue(AssetProperty.SYNTHETIC_ASSET_ID) as string;
return this.createWrapper(pageElement, <Placeholder src={src} />);
}
Expand All @@ -107,7 +108,7 @@ class Preview extends React.Component<PreviewProps> {
* @returns A React component in case of a page element, the primitive in case of a primitive,
* or an array or object with values converted in the same manner, if an array resp. object is provided.
*/
private createComponent(value: PropertyValue): JSX.Element | PropertyValue {
protected createComponent(value: PropertyValue): JSX.Element | PropertyValue {
if (value === undefined || value === null || typeof value !== 'object') {
// Primitives stay primitives.
return value;
Expand All @@ -129,21 +130,9 @@ class Preview extends React.Component<PreviewProps> {
return this.createStringComponent(pageElement);
} else if (patternId === Pattern.SYNTHETIC_ASSET_ID) {
return this.createAssetComponent(pageElement);
} else {
return this.createPatternComponent(pageElement, patternId);
}

// tslint:disable-next-line:no-any
const componentProps: any = {};
this.collectPropertyValues(componentProps, pageElement);
this.collectChildren(componentProps, pageElement);

// Then, load the pattern factory
const patternFactory:
| React.StatelessComponent
| ObjectConstructor = this.loadAndCachePatternFactory(pattern);

// Finally, build the component and wrap it for selectability
const reactElement = React.createElement(patternFactory, componentProps);
return this.createWrapper(pageElement, reactElement);
} catch (error) {
return <ErrorMessage patternName={pageElement.getName()} error={error.toString()} />;
}
Expand All @@ -161,11 +150,33 @@ class Preview extends React.Component<PreviewProps> {
}
}

private createStringComponent(pageElement: PageElement): string {
protected createPatternComponent(pageElement: PageElement, patternId: string): JSX.Element {
// tslint:disable-next-line:no-any
const patternProps: any = {};
this.collectPropertyValues(patternProps, pageElement);
this.collectChildren(patternProps, pageElement);

// Then, load the pattern factory
const patternFactory:
| React.StatelessComponent
| ObjectConstructor
| undefined = this.props.patternFactories.get(patternId);
if (!patternFactory) {
throw new Error(`Unknown pattern ID ${patternId}`);
}

// Finally, build the component and wrap it for selectability
const reactElement = (
<PatternComponent patternFactory={patternFactory} patternProps={patternProps} />
);
return this.createWrapper(pageElement, reactElement);
}

protected createStringComponent(pageElement: PageElement): string {
return String(pageElement.getPropertyValue(StringProperty.SYNTHETIC_TEXT_ID));
}

private createWrapper(pageElement: PageElement, reactElement: JSX.Element): JSX.Element {
protected createWrapper(pageElement: PageElement, reactElement: JSX.Element): JSX.Element {
return (
<PatternWrapper
key={pageElement.getId()}
Expand All @@ -181,23 +192,6 @@ class Preview extends React.Component<PreviewProps> {
);
}

private loadAndCachePatternFactory(
pattern: Pattern
): React.StatelessComponent | ObjectConstructor {
let patternFactory: React.StatelessComponent | ObjectConstructor = this.patternFactories[
pattern.getId()
];
if (patternFactory == null) {
const patternPath: string = pattern.getImplementationPath();
const exportName = pattern.getExportName();
const module = require(patternPath);
patternFactory = module[exportName];
this.patternFactories[pattern.getId()] = patternFactory;
}

return patternFactory;
}

public render(): JSX.Element | null {
if (this.props.page) {
const highlightAreaProps = this.highlightArea.getProps();
Expand Down Expand Up @@ -241,8 +235,78 @@ class Preview extends React.Component<PreviewProps> {

@observer
export class PreviewApp extends React.Component<{}, PreviewAppState> {
@MobX.observable
private patternFactories: Map<string, React.StatelessComponent | ObjectConstructor> = new Map();

private patternReloadScheduled: boolean = false;
private patternWatcher?: Chokidar.FSWatcher;
private patternWatcherPath: string | undefined;

public constructor(props: {}) {
super(props);
this.loadAndWatchPatternFactories();
}

@MobX.action
private loadAndWatchPatternFactories(): void {
this.patternFactories.clear();
const styleguide = Store.getInstance().getStyleguide();
if (styleguide) {
styleguide.getPatterns().forEach(pattern => {
if (pattern.getId().startsWith('synthetic:')) {
return;
}

try {
const patternPath: string = pattern.getImplementationPath();
const exportName = pattern.getExportName();

// Ensure that require does not cache the implementation,
// so that we still have control on when we reload it
delete require.cache[require.resolve(patternPath)];

const module = require(patternPath);
this.patternFactories.set(pattern.getId(), module[exportName]);
} catch (error) {
console.warn(`Failed to load pattern ${pattern.getId()}: ${error}`);
}
});
}

const patternWatcherPath = styleguide
? styleguide
.getPatternsPath()
.split(PathUtils.sep)
.join('/')
: undefined;

if (this.patternWatcherPath !== patternWatcherPath) {
if (this.patternWatcher) {
this.patternWatcher.close();
this.patternWatcher = undefined;
}

if (patternWatcherPath) {
const watchPaths = [
`${patternWatcherPath}/**/index.js`,
`${patternWatcherPath}/**/index.d.ts`
];
this.patternWatcher = Chokidar.watch(watchPaths);
this.patternWatcher.on('all', (event: string | symbol) => {
if (this.patternReloadScheduled) {
return;
}

this.patternReloadScheduled = true;
global.setTimeout(() => {
this.patternReloadScheduled = false;
this.loadAndWatchPatternFactories();
}, 100);
});
}

this.patternWatcherPath = patternWatcherPath;
}
}

public render(): JSX.Element {
Expand All @@ -259,6 +323,7 @@ export class PreviewApp extends React.Component<{}, PreviewAppState> {
<div>
<Preview
page={Store.getInstance().getCurrentPage()}
patternFactories={this.patternFactories}
selectedElementId={selectedElement && selectedElement.getId()}
/>
{DevTools ? <DevTools /> : ''}
Expand Down

0 comments on commit f30d832

Please sign in to comment.