Skip to content

refactor: update knobs types, simplify logic #89

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

Merged
merged 7 commits into from
Nov 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
199 changes: 106 additions & 93 deletions src/api-demo-layout-mixin.ts → src/api-demo-knobs-mixin.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { LitElement, PropertyValues } from 'lit';
import { property } from 'lit/decorators/property.js';
import { getSlotDefault } from './lib/knobs.js';
import { getSlotDefault, Knob } from './lib/knobs.js';
import {
ComponentWithProps,
CSSPropertyInfo,
PropertyInfo,
SlotInfo,
SlotValue,
EventInfo,
KnobValues,
KnobValue
EventInfo
} from './lib/types.js';
import {
getTemplates,
Expand All @@ -24,10 +22,10 @@ import {
const { HOST, KNOB } = TemplateTypes;

const getDefault = (
prop: PropertyInfo
prop: Knob<PropertyInfo>
): string | number | boolean | null | undefined => {
const { type, default: value } = prop;
switch (normalizeType(type)) {
const { knobType, default: value } = prop;
switch (knobType) {
case 'boolean':
return value !== 'false';
case 'number':
Expand Down Expand Up @@ -61,13 +59,78 @@ const isGetter = (
return result;
};

const getKnobs = (
tag: string,
props: PropertyInfo[],
exclude = ''
): Knob<PropertyInfo>[] => {
// Exclude getters and specific properties
let propKnobs = props.filter(
({ name }) =>
!exclude.includes(name) && !isGetter(customElements.get(tag), name)
) as Knob<PropertyInfo>[];

// Set knob types and default knobs values
propKnobs = propKnobs.map((prop) => {
const knob = {
...prop,
knobType: normalizeType(prop.type)
};

if (typeof knob.default === 'string') {
knob.value = getDefault(knob);
}

return knob;
});

return propKnobs;
};

const getCustomKnobs = (tag: string, vid?: number): Knob<PropertyInfo>[] => {
return getTemplates(vid as number, tag, KNOB)
.map((template) => {
const { attr, type } = template.dataset;
let result = null;
if (attr) {
if (type === 'select') {
const node = getTemplateNode(template);
const options = node
? Array.from(node.children)
.filter(
(c): c is HTMLOptionElement => c instanceof HTMLOptionElement
)
.map((option) => option.value)
: [];
if (node instanceof HTMLSelectElement && options.length > 1) {
result = {
name: attr,
attribute: attr,
knobType: type,
options
};
}
}
if (type === 'string' || type === 'boolean') {
result = {
name: attr,
attribute: attr,
knobType: type
};
}
}
return result as Knob<PropertyInfo>;
})
.filter(Boolean);
};

/* eslint-disable @typescript-eslint/no-explicit-any */
export type Constructor<T = unknown> = new (...args: any[]) => T;

export interface ApiDemoLayoutInterface {
export interface ApiDemoKnobsInterface {
tag: string;
props: PropertyInfo[];
finalProps: PropertyInfo[];
propKnobs: Knob<PropertyInfo>[];
slots: SlotInfo[];
events: EventInfo[];
cssProps: CSSPropertyInfo[];
Expand All @@ -76,17 +139,17 @@ export interface ApiDemoLayoutInterface {
processedSlots: SlotValue[];
processedCss: CSSPropertyInfo[];
eventLog: CustomEvent[];
customKnobs: PropertyInfo[];
knobs: KnobValues;
customKnobs: Knob<PropertyInfo>[];
knobs: Record<string, Knob>;
setKnobs(target: HTMLInputElement): void;
setSlots(target: HTMLInputElement): void;
setCss(target: HTMLInputElement): void;
onRendered(event: CustomEvent): void;
}

export const ApiDemoLayoutMixin = <T extends Constructor<LitElement>>(
export const ApiDemoKnobsMixin = <T extends Constructor<LitElement>>(
base: T
): T & Constructor<ApiDemoLayoutInterface> => {
): T & Constructor<ApiDemoKnobsInterface> => {
class DemoLayout extends base {
@property() tag = '';

Expand Down Expand Up @@ -116,13 +179,13 @@ export const ApiDemoLayoutMixin = <T extends Constructor<LitElement>>(
eventLog!: CustomEvent[];

@property({ attribute: false })
customKnobs: PropertyInfo[] = [];
customKnobs: Knob<PropertyInfo>[] = [];

@property({ attribute: false })
knobs: KnobValues = {};
knobs: Record<string, Knob> = {};

@property({ attribute: false })
finalProps!: PropertyInfo[];
propKnobs!: Knob<PropertyInfo>[];

willUpdate(props: PropertyValues) {
// Reset state if tag changed
Expand All @@ -131,30 +194,10 @@ export const ApiDemoLayoutMixin = <T extends Constructor<LitElement>>(
this.eventLog = [];
this.processedCss = [];
this.processedSlots = [];

// Store properties without getters
this.finalProps = this.props.filter(
({ name }) => !isGetter(customElements.get(this.tag), name)
);

this.customKnobs = this._getCustomKnobs();
this.propKnobs = getKnobs(this.tag, this.props, this.exclude);
this.customKnobs = getCustomKnobs(this.tag, this.vid);
}

if (props.has('exclude')) {
this.finalProps = this.finalProps
.filter(({ name }) => !this.exclude.includes(name))
.map((prop: PropertyInfo) => {
return typeof prop.default === 'string'
? {
...prop,
value: getDefault(prop)
}
: prop;
});
}
}

protected updated(props: PropertyValues): void {
if (props.has('slots') && this.slots) {
this.processedSlots = this.slots
.sort((a: SlotInfo, b: SlotInfo) => {
Expand All @@ -175,53 +218,18 @@ export const ApiDemoLayoutMixin = <T extends Constructor<LitElement>>(
}
}

private _getCustomKnobs(): PropertyInfo[] {
return getTemplates(this.vid as number, this.tag, KNOB)
.map((template) => {
const { attr, type } = template.dataset;
let result = null;
if (attr) {
if (type === 'select') {
const node = getTemplateNode(template);
const options = node
? Array.from(node.children)
.filter(
(c): c is HTMLOptionElement =>
c instanceof HTMLOptionElement
)
.map((option) => option.value)
: [];
if (node instanceof HTMLSelectElement && options.length > 1) {
result = {
name: attr,
attribute: attr,
type,
options
};
}
}
if (type === 'string' || type === 'boolean') {
result = {
name: attr,
attribute: attr,
type
};
}
}
return result;
})
.filter(Boolean) as PropertyInfo[];
}

private _getProp(name: string): { prop?: PropertyInfo; custom?: boolean } {
private _getProp(name: string): {
prop: Knob<PropertyInfo>;
custom?: boolean;
} {
const isMatch = isPropMatch(name);
const prop = this.finalProps.find(isMatch);
return prop
? { prop }
: {
prop: this.customKnobs.find(isMatch),
custom: true
};
let prop = this.propKnobs.find(isMatch);
let custom = false;
if (!prop) {
prop = this.customKnobs.find(isMatch) as Knob<PropertyInfo>;
custom = true;
}
return { prop, custom };
}

setCss(target: HTMLInputElement): void {
Expand All @@ -240,7 +248,7 @@ export const ApiDemoLayoutMixin = <T extends Constructor<LitElement>>(
setKnobs(target: HTMLInputElement): void {
const { name, type } = target.dataset;
let value;
switch (normalizeType(type)) {
switch (type) {
case 'boolean':
value = target.checked;
break;
Expand All @@ -256,7 +264,12 @@ export const ApiDemoLayoutMixin = <T extends Constructor<LitElement>>(
const { attribute } = prop;
this.knobs = {
...this.knobs,
[name as string]: { type, value, attribute, custom } as KnobValue
[name as string]: {
knobType: type,
value,
attribute,
custom
} as Knob<PropertyInfo>
};
}
}
Expand All @@ -280,13 +293,13 @@ export const ApiDemoLayoutMixin = <T extends Constructor<LitElement>>(

if (hasTemplate(this.vid as number, this.tag, HOST)) {
// Apply property values from template
this.finalProps
this.propKnobs
.filter((prop) => {
const { name, type } = prop;
const { name, knobType } = prop;
const defaultValue = getDefault(prop);
return (
component[name] !== defaultValue ||
(normalizeType(type) === 'boolean' && defaultValue)
(knobType === 'boolean' && defaultValue)
);
})
.forEach((prop) => {
Expand Down Expand Up @@ -326,17 +339,17 @@ export const ApiDemoLayoutMixin = <T extends Constructor<LitElement>>(
}
}

private _syncKnob(component: Element, changed: PropertyInfo): void {
const { name, type, attribute } = changed;
private _syncKnob(component: Element, changed: Knob<PropertyInfo>): void {
const { name, knobType, attribute } = changed;
const value = (component as unknown as ComponentWithProps)[name];

// update knobs to avoid duplicate event
this.knobs = {
...this.knobs,
[name]: { type, value, attribute }
[name]: { knobType, value, attribute }
};

this.finalProps = this.finalProps.map((prop) => {
this.propKnobs = this.propKnobs.map((prop) => {
return prop.name === name
? {
...prop,
Expand Down
13 changes: 4 additions & 9 deletions src/api-viewer-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import {
slotRenderer
} from './lib/knobs.js';
import { hasTemplate, TemplateTypes } from './lib/utils.js';
import { ApiDemoLayoutMixin } from './api-demo-layout-mixin.js';
import { ApiDemoKnobsMixin } from './api-demo-knobs-mixin.js';
import './api-viewer-panel.js';
import './api-viewer-tab.js';
import './api-viewer-tabs.js';

class ApiViewerDemo extends ApiDemoLayoutMixin(LitElement) {
class ApiViewerDemo extends ApiDemoKnobsMixin(LitElement) {
@property() copyBtnText = 'copy';

private _whenDefined: Record<string, Promise<unknown>> = {};
Expand Down Expand Up @@ -49,7 +49,7 @@ class ApiViewerDemo extends ApiDemoLayoutMixin(LitElement) {
this.events,
this.slots,
this.customKnobs,
this.finalProps
this.propKnobs
].map((arr) => arr.length === 0);

const id = this.vid as number;
Expand Down Expand Up @@ -92,12 +92,7 @@ class ApiViewerDemo extends ApiDemoLayoutMixin(LitElement) {
<api-viewer-panel slot="panel" part="tab-panel">
<div part="knobs">
<section part="knobs-column" @change=${this._onPropChanged}>
${renderKnobs(
this.finalProps,
'Properties',
'prop',
propRenderer
)}
${renderKnobs(this.propKnobs, 'Properties', 'prop', propRenderer)}
${renderKnobs(
this.customKnobs,
'Attributes',
Expand Down
9 changes: 5 additions & 4 deletions src/lib/demo-snippet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { htmlRender } from 'highlight-ts/es/render/html';
import { registerLanguages } from 'highlight-ts/es/languages';
import { XML } from 'highlight-ts/es/languages/xml';
import { init, process } from 'highlight-ts/es/process';
import { CSSPropertyInfo, KnobValues, SlotValue } from './types.js';
import { CSSPropertyInfo, SlotValue } from './types.js';
import { Knob } from './knobs.js';
import { CSS } from './highlight-css.js';
import {
getTemplate,
Expand Down Expand Up @@ -52,7 +53,7 @@ const getTplContent = (
export const renderSnippet = (
id: number,
tag: string,
values: KnobValues,
values: Record<string, Knob>,
slots: SlotValue[],
cssProps: CSSPropertyInfo[]
): TemplateResult => {
Expand Down Expand Up @@ -80,9 +81,9 @@ export const renderSnippet = (
Object.keys(values)
.sort((a, b) => (a > b ? 1 : -1))
.forEach((key: string) => {
const { value, type, attribute } = values[key];
const { value, knobType, attribute } = values[key];
const attr = attribute || key;
switch (normalizeType(type)) {
switch (normalizeType(knobType)) {
case 'boolean':
markup += value ? ` ${attr}` : '';
break;
Expand Down
Loading