Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core: Allow overriding WebView and UrlStore #19623

Merged
merged 24 commits into from
Nov 8, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6998198
Allow overriding WebView and UrlStore
tmeasday Oct 25, 2022
c4a69ab
Create super classes for Selection/View
tmeasday Oct 25, 2022
0f04209
Default `PreviewWeb`
tmeasday Oct 26, 2022
5ffdac2
Fix typing problems
tmeasday Oct 26, 2022
f286554
One more fix
tmeasday Oct 26, 2022
3b96208
Fix test
tmeasday Oct 26, 2022
b976e0b
Merge remote-tracking branch 'origin/next' into tom/sb-850-allow-pass…
tmeasday Oct 31, 2022
2c92e6e
Rename `renderToDOM` -> `renderToRoot`
tmeasday Oct 31, 2022
1655930
Shadow CSF's `ProjectAnnotations` with our own.
tmeasday Nov 1, 2022
816930c
Fix various missed types
tmeasday Nov 1, 2022
a7f29b7
Add `rootElement` to `Framework` and add `Framework`
tmeasday Nov 2, 2022
95da68c
Drop second parameter, and read root element type off framework
tmeasday Nov 2, 2022
9a55d70
Update ember framework
tmeasday Nov 2, 2022
b00748b
Fix linting
shilman Nov 2, 2022
a29323d
Update to `renderToCanvas` + `canvasElement`
tmeasday Nov 4, 2022
6d96f7b
Update deprecate
tmeasday Nov 4, 2022
4b481fb
Split `PreviewWeb` into `PreviewWithSelection`
tmeasday Nov 4, 2022
8290a16
Move away from abstract classes in favour of interfaces
tmeasday Nov 4, 2022
2becfed
Merge remote-tracking branch 'origin/next' into tom/sb-850-allow-pass…
tmeasday Nov 4, 2022
91fa988
Update renderToCanvas functions to use precise type
tmeasday Nov 4, 2022
5a4698a
Rename `domElement` to `canvasElement`
tmeasday Nov 4, 2022
bfc21a8
Actually export `PreviewWithSelection`
tmeasday Nov 4, 2022
98b4948
Fix mistakes
tmeasday Nov 4, 2022
2c90d6b
More fixes
tmeasday Nov 4, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions code/lib/preview-web/src/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const STORY_INDEX_PATH = './index.json';

export type MaybePromise<T> = Promise<T> | T;

export class Preview<TFramework extends AnyFramework> {
export class Preview<TFramework extends AnyFramework, TRootElement = HTMLElement> {
serverChannel?: Channel;

storyStore: StoryStore<TFramework>;
Expand All @@ -51,9 +51,9 @@ export class Preview<TFramework extends AnyFramework> {

importFn?: Store_ModuleImportFn;

renderToDOM?: Store_RenderToDOM<TFramework>;
renderToDOM?: Store_RenderToDOM<TFramework, TRootElement>;

storyRenders: StoryRender<TFramework>[] = [];
storyRenders: StoryRender<TFramework, TRootElement>[] = [];

previewEntryError?: Error;

Expand Down Expand Up @@ -81,7 +81,9 @@ export class Preview<TFramework extends AnyFramework> {
// getProjectAnnotations has been run, thus this slightly awkward approach
getStoryIndex?: () => Store_StoryIndex;
importFn: Store_ModuleImportFn;
getProjectAnnotations: () => MaybePromise<Store_WebProjectAnnotations<TFramework>>;
getProjectAnnotations: () => MaybePromise<
Store_WebProjectAnnotations<TFramework, TRootElement>
>;
}) {
// We save these two on initialization in case `getProjectAnnotations` errors,
// in which case we may need them later when we recover.
Expand All @@ -106,7 +108,7 @@ export class Preview<TFramework extends AnyFramework> {
}

getProjectAnnotationsOrRenderError(
getProjectAnnotations: () => MaybePromise<Store_WebProjectAnnotations<TFramework>>
getProjectAnnotations: () => MaybePromise<Store_WebProjectAnnotations<TFramework, TRootElement>>
): Store_PromiseLike<ProjectAnnotations<TFramework>> {
return SynchronousPromise.resolve()
.then(getProjectAnnotations)
Expand All @@ -132,7 +134,9 @@ export class Preview<TFramework extends AnyFramework> {
}

// If initialization gets as far as project annotations, this function runs.
initializeWithProjectAnnotations(projectAnnotations: Store_WebProjectAnnotations<TFramework>) {
initializeWithProjectAnnotations(
projectAnnotations: Store_WebProjectAnnotations<TFramework, TRootElement>
) {
this.storyStore.setProjectAnnotations(projectAnnotations);

this.setInitialGlobals();
Expand Down Expand Up @@ -305,11 +309,11 @@ export class Preview<TFramework extends AnyFramework> {
// main to be consistent with the previous behaviour. In the future,
// we will change it to go ahead and load the story, which will end up being
// "instant", although async.
renderStoryToElement(story: Store_Story<TFramework>, element: HTMLElement) {
renderStoryToElement(story: Store_Story<TFramework>, element: TRootElement) {
if (!this.renderToDOM)
throw new Error(`Cannot call renderStoryToElement before initialization`);

const render = new StoryRender<TFramework>(
const render = new StoryRender<TFramework, TRootElement>(
this.channel,
this.storyStore,
this.renderToDOM,
Expand All @@ -329,9 +333,9 @@ export class Preview<TFramework extends AnyFramework> {

async teardownRender(
render:
| StoryRender<TFramework>
| TemplateDocsRender<TFramework>
| StandaloneDocsRender<TFramework>,
| StoryRender<TFramework, TRootElement>
| TemplateDocsRender<TFramework, TRootElement>
| StandaloneDocsRender<TFramework, TRootElement>,
{ viewModeChanged }: { viewModeChanged?: boolean } = {}
) {
this.storyRenders = this.storyRenders.filter((r) => r !== render);
Expand Down
100 changes: 64 additions & 36 deletions code/lib/preview-web/src/PreviewWeb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import { PREPARE_ABORTED } from './render/Render';
import { StoryRender } from './render/StoryRender';
import { TemplateDocsRender } from './render/TemplateDocsRender';
import { StandaloneDocsRender } from './render/StandaloneDocsRender';
import { SelectionStore } from './SelectionStore';
import { View } from './View';

const { window: globalWindow } = global;

Expand All @@ -49,33 +51,45 @@ function focusInInput(event: Event) {
return /input|textarea/i.test(target.tagName) || target.getAttribute('contenteditable') !== null;
}

type PossibleRender<TFramework extends AnyFramework> =
| StoryRender<TFramework>
| TemplateDocsRender<TFramework>
| StandaloneDocsRender<TFramework>;
type PossibleRender<TFramework extends AnyFramework, TRootElement = HTMLElement> =
| StoryRender<TFramework, TRootElement>
| TemplateDocsRender<TFramework, TRootElement>
| StandaloneDocsRender<TFramework, TRootElement>;

function isStoryRender<TFramework extends AnyFramework>(
r: PossibleRender<TFramework>
): r is StoryRender<TFramework> {
function isStoryRender<TFramework extends AnyFramework, TRootElement>(
r: PossibleRender<TFramework, TRootElement>
): r is StoryRender<TFramework, TRootElement> {
return r.type === 'story';
}

export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramework> {
urlStore: UrlStore;
export class PreviewWeb<
tmeasday marked this conversation as resolved.
Show resolved Hide resolved
TFramework extends AnyFramework,
TRootElement = HTMLElement
> extends Preview<TFramework, TRootElement> {
selectionStore: SelectionStore;

view: WebView;
view: View<TRootElement>;

previewEntryError?: Error;

currentSelection?: Store_Selection;

currentRender?: PossibleRender<TFramework>;
currentRender?: PossibleRender<TFramework, TRootElement>;

constructor() {
constructor({
// I'm not quite sure how to express this -- if you don't pass a view, you need to ensure
// this is a PreviewWeb<..., HTMLElement>. If you pass one, it can be parameterized however you want,
// as long as it is consistent.
view = new WebView() as any,
selectionStore = new UrlStore(),
}: {
view: View<TRootElement>;
selectionStore: SelectionStore;
}) {
super();

this.view = new WebView();
this.urlStore = new UrlStore();
this.view = view;
this.selectionStore = selectionStore;
}

setupListeners() {
Expand All @@ -88,7 +102,9 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
this.channel.on(PRELOAD_ENTRIES, this.onPreloadStories.bind(this));
}

initializeWithProjectAnnotations(projectAnnotations: Store_WebProjectAnnotations<TFramework>) {
initializeWithProjectAnnotations(
projectAnnotations: Store_WebProjectAnnotations<TFramework, TRootElement>
) {
return super
.initializeWithProjectAnnotations(projectAnnotations)
.then(() => this.setInitialGlobals());
Expand All @@ -98,7 +114,7 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
if (!this.storyStore.globals)
throw new Error(`Cannot call setInitialGlobals before initialization`);

const { globals } = this.urlStore.selectionSpecifier || {};
const { globals } = this.selectionStore.selectionSpecifier || {};
if (globals) {
this.storyStore.globals.updateFromPersisted(globals);
}
Expand All @@ -121,12 +137,12 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
if (!this.storyStore.storyIndex)
throw new Error(`Cannot call selectSpecifiedStory before initialization`);

if (!this.urlStore.selectionSpecifier) {
if (!this.selectionStore.selectionSpecifier) {
this.renderMissingStory();
return;
}

const { storySpecifier, args } = this.urlStore.selectionSpecifier;
const { storySpecifier, args } = this.selectionStore.selectionSpecifier;
const entry = this.storyStore.storyIndex.entryFromSpecifier(storySpecifier);

if (!entry) {
Expand Down Expand Up @@ -155,10 +171,10 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
}

const { id: storyId, type: viewMode } = entry;
this.urlStore.setSelection({ storyId, viewMode });
this.channel.emit(STORY_SPECIFIED, this.urlStore.selection);
this.selectionStore.setSelection({ storyId, viewMode });
this.channel.emit(STORY_SPECIFIED, this.selectionStore.selection);

this.channel.emit(CURRENT_STORY_WAS_SET, this.urlStore.selection);
this.channel.emit(CURRENT_STORY_WAS_SET, this.selectionStore.selection);

await this.renderSelection({ persistedArgs: args });
}
Expand All @@ -173,7 +189,7 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
}) {
await super.onGetProjectAnnotationsChanged({ getProjectAnnotations });

if (this.urlStore.selection) {
if (this.selectionStore.selection) {
this.renderSelection();
}
}
Expand All @@ -192,7 +208,7 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
this.channel.emit(SET_INDEX, await this.storyStore.getSetIndexPayload());
}

if (this.urlStore.selection) {
if (this.selectionStore.selection) {
await this.renderSelection();
} else {
// Our selection has never applied before, but maybe it does now, let's try!
Expand All @@ -213,13 +229,13 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
async onSetCurrentStory(selection: { storyId: StoryId; viewMode?: ViewMode }) {
await this.storyStore.initializationPromise;

this.urlStore.setSelection({ viewMode: 'story', ...selection });
this.channel.emit(CURRENT_STORY_WAS_SET, this.urlStore.selection);
this.selectionStore.setSelection({ viewMode: 'story', ...selection });
this.channel.emit(CURRENT_STORY_WAS_SET, this.selectionStore.selection);
this.renderSelection();
}

onUpdateQueryParams(queryParams: any) {
this.urlStore.setQueryParams(queryParams);
this.selectionStore.setQueryParams(queryParams);
}

async onUpdateGlobals({ globals }: { globals: Globals }) {
Expand Down Expand Up @@ -256,7 +272,7 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
async renderSelection({ persistedArgs }: { persistedArgs?: Args } = {}) {
const { renderToDOM } = this;
if (!renderToDOM) throw new Error('Cannot call renderSelection before initialization');
const { selection } = this.urlStore;
const { selection } = this.selectionStore;
if (!selection) throw new Error('Cannot call renderSelection as no selection was made');

const { storyId } = selection;
Expand Down Expand Up @@ -289,12 +305,12 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
await this.teardownRender(this.currentRender);
}

let render: PossibleRender<TFramework>;
let render: PossibleRender<TFramework, TRootElement>;
if (entry.type === 'story') {
render = new StoryRender<TFramework>(
render = new StoryRender<TFramework, TRootElement>(
this.channel,
this.storyStore,
(...args) => {
(...args: Parameters<typeof renderToDOM>) => {
// At the start of renderToDOM we make the story visible (see note in WebView)
this.view.showStoryDuringRender();
return renderToDOM(...args);
Expand All @@ -304,9 +320,17 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
'story'
);
} else if (entry.standalone) {
render = new StandaloneDocsRender<TFramework>(this.channel, this.storyStore, entry);
render = new StandaloneDocsRender<TFramework, TRootElement>(
this.channel,
this.storyStore,
entry
);
} else {
render = new TemplateDocsRender<TFramework>(this.channel, this.storyStore, entry);
render = new TemplateDocsRender<TFramework, TRootElement>(
this.channel,
this.storyStore,
entry
);
}

// We need to store this right away, so if the story changes during
Expand Down Expand Up @@ -385,20 +409,24 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew

if (isStoryRender(render)) {
if (!render.story) throw new Error('Render has not been prepared!');
this.storyRenders.push(render as StoryRender<TFramework>);
(this.currentRender as StoryRender<TFramework>).renderToElement(
this.storyRenders.push(render as StoryRender<TFramework, TRootElement>);
(this.currentRender as StoryRender<TFramework, TRootElement>).renderToElement(
this.view.prepareForStory(render.story)
);
} else {
this.currentRender.renderToElement(
this.view.prepareForDocs(),
this.renderStoryToElement.bind(this)
// This argument is used for docs, which is currently only compatible with HTMLElements
this.renderStoryToElement.bind(this) as PreviewWeb<
TFramework,
HTMLElement
>['renderStoryToElement']
);
}
}

async teardownRender(
render: PossibleRender<TFramework>,
render: PossibleRender<TFramework, TRootElement>,
{ viewModeChanged = false }: { viewModeChanged?: boolean } = {}
) {
this.storyRenders = this.storyRenders.filter((r) => r !== render);
Expand Down
12 changes: 12 additions & 0 deletions code/lib/preview-web/src/SelectionStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* eslint-disable camelcase */
import type { Store_SelectionSpecifier, Store_Selection } from '@storybook/types';

export abstract class SelectionStore {
public selectionSpecifier: Store_SelectionSpecifier | null;

public selection?: Store_Selection;

abstract setSelection(selection: Store_Selection): void;

abstract setQueryParams(queryParams: qs.ParsedQs): void;
}
4 changes: 3 additions & 1 deletion code/lib/preview-web/src/UrlStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import qs from 'qs';
import type { ViewMode, Store_SelectionSpecifier, Store_Selection } from '@storybook/types';

import { parseArgsParam } from './parseArgsParam';
import { SelectionStore } from './SelectionStore';

const { history, document } = global;

Expand Down Expand Up @@ -84,12 +85,13 @@ export const getSelectionSpecifierFromPath: () => Store_SelectionSpecifier | nul
return null;
};

export class UrlStore {
export class UrlStore extends SelectionStore {
selectionSpecifier: Store_SelectionSpecifier | null;

selection?: Store_Selection;

constructor() {
super();
this.selectionSpecifier = getSelectionSpecifierFromPath();
}

Expand Down
25 changes: 25 additions & 0 deletions code/lib/preview-web/src/View.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* eslint-disable camelcase */
import type { Store_Story } from '@storybook/types';

export abstract class View<TRootElement> {
// Get ready to render a story, returning the element to render to
abstract prepareForStory(story: Store_Story<any>): TRootElement;

abstract prepareForDocs(): TRootElement;

abstract showErrorDisplay({ message = '', stack = '' }): void;

abstract showNoPreview(): void;

abstract showPreparingStory({ immediate = false }): void;

abstract showPreparingDocs(): void;

abstract showMain(): void;

abstract showDocs(): void;

abstract showStory(): void;

abstract showStoryDuringRender(): void;
}
Loading