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

Feat: styles #81

Merged
merged 1 commit into from
Nov 11, 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
2 changes: 1 addition & 1 deletion src/bootloader-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const qwikLoader = (doc: Document, hasInitialized?: boolean | number) =>
if (url) {
const handler = getModuleExport(
url,
(window as any)[url.pathname] || (await import(String(url)))
(window as any)[url.pathname] || (await import(String(url).split('#')[0]))
);
handler(element, ev, url);
}
Expand Down
9 changes: 5 additions & 4 deletions src/core/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,24 +175,25 @@ export interface QComponent<PROPS extends {} = any, STATE extends {} = any> exte
styleClass: string | null;
// (undocumented)
styleHostClass: string | null;
// Warning: (ae-forgotten-export) The symbol "QrlStyles" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "QStyles" needs to be exported by the entry point index.d.ts
//
// (undocumented)
styles: QrlStyles<any> | null;
styles: QStyles<any> | null;
// (undocumented)
tag: string;
}

// @public
export function qComponent<PROPS = {}, STATE = {}>({ onRender, styles, tagName, props, onResume, onMount, onUnmount, onHydrate, onDehydrate, }: {
export function qComponent<PROPS = {}, STATE = {}>({ onRender, styles, unscopedStyles, tagName, props, onResume, onMount, onUnmount, onHydrate, onDehydrate, }: {
onRender: QHook<PROPS, STATE, undefined, any>;
tagName?: string;
onMount?: QHook<PROPS, undefined, undefined, STATE>;
onUnmount?: QHook<PROPS, STATE, undefined, void> | null;
onDehydrate?: QHook<PROPS, STATE, undefined, void> | null;
onHydrate?: QHook<PROPS, STATE, undefined, void> | null;
onResume?: QHook<PROPS, STATE, undefined, void> | null;
styles?: QrlStyles<any>;
styles?: QStyles<any>;
unscopedStyles?: QStyles<any>;
props?: PROPS;
}): QComponent<PROPS, STATE>;

Expand Down
40 changes: 32 additions & 8 deletions src/core/component/q-component-ctx.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { assertDefined } from '../assert/assert';
import { cursorForComponent, cursorReconcileEnd } from '../render/cursor';
import { qImport } from '../import/qImport';
import { _stateQObject } from '../object/q-object';
import type { OnHookReturn } from '../props/q-props';
import { qProps } from '../props/q-props.public';
import { cursorForComponent, cursorReconcileEnd } from '../render/cursor';
import { ComponentRenderQueue, visitJsxNode } from '../render/q-render';
import { AttributeMarker } from '../util/markers';
import { flattenPromiseTree } from '../util/promises';
import { QrlStyles, styleContent, styleHost } from './qrl-styles';
import { _stateQObject } from '../object/q-object';
import { qProps } from '../props/q-props.public';
import { styleContent, styleHost, styleKey } from './qrl-styles';

// TODO(misko): Can we get rid of this whole file, and instead teach qProps to know how to render
// the advantage will be that the render capability would then be exposed to the outside world as well.
Expand All @@ -21,10 +22,12 @@ export class QComponentCtx {

constructor(hostElement: HTMLElement) {
this.hostElement = hostElement;
const styleId = (this.styleId = hostElement.getAttribute(AttributeMarker.ComponentStyles));
if (styleId) {
this.styleHostClass = styleHost(styleId as any as QrlStyles<any>);
this.styleClass = styleContent(styleId as any as QrlStyles<any>);
const scopedStyleId = (this.styleId = styleKey(
hostElement.getAttribute(AttributeMarker.ComponentStyles) as any
)!);
if (scopedStyleId) {
this.styleHostClass = styleHost(scopedStyleId);
this.styleClass = styleContent(scopedStyleId);
}
}

Expand All @@ -37,6 +40,10 @@ export class QComponentCtx {
// TODO(misko): extract constant
if (props['state:'] == null) {
try {
const scopedStyle: string | null = props[AttributeMarker.ComponentStyles];
const unscopedStyle: string | null = props[AttributeMarker.ComponentUnscopedStyles];
insertStyleIfNeeded(this, scopedStyle);
insertStyleIfNeeded(this, unscopedStyle);
const hook = props['on:qMount'];
if (hook) {
const values: OnHookReturn[] = await hook('qMount');
Expand Down Expand Up @@ -101,3 +108,20 @@ export function getHostElement(element: Element): HTMLElement | null {
}
return element as HTMLElement | null;
}

function insertStyleIfNeeded(ctx: QComponentCtx, style: string | null) {
if (style) {
const styleId = styleKey(style as any)!;
const document = ctx.hostElement.ownerDocument;
const head = document.querySelector('head')!;
if (!head.querySelector(`style[q\\:style="${styleId}"]`)) {
const styleImport = Promise.resolve(qImport<string>(document, style));
styleImport.then((styles: string) => {
const style = document.createElement('style');
style.setAttribute('q:style', styleId);
style.textContent = styles.replace(/�/g, styleId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is /�/g?

head.appendChild(style);
});
}
}
}
14 changes: 9 additions & 5 deletions src/core/component/q-component.public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { HTMLAttributes } from '../render/jsx/types/jsx-generated';
import type { FunctionComponent, JSXNode } from '../render/jsx/types/jsx-node';
import { AttributeMarker } from '../util/markers';
import type { QHook } from './qrl-hook.public';
import { QrlStyles, styleContent, styleHost } from './qrl-styles';
import { QStyles, styleContent, styleHost, styleKey } from './qrl-styles';

// <docs markdown="./q-component.md#qComponent">
// !!DO NOT EDIT THIS COMMENT DIRECTLY!!! (edit ./q-component.md instead)
Expand Down Expand Up @@ -69,6 +69,7 @@ import { QrlStyles, styleContent, styleHost } from './qrl-styles';
export function qComponent<PROPS = {}, STATE = {}>({
onRender,
styles,
unscopedStyles,
tagName,
props,
onResume,
Expand Down Expand Up @@ -204,7 +205,8 @@ export function qComponent<PROPS = {}, STATE = {}>({
*/
// </docs>
// TODO(misko): finish documentation once implemented.
styles?: QrlStyles<any>;
styles?: QStyles<any>;
unscopedStyles?: QStyles<any>;

// <docs markdown="./q-component.md#qComponent.props">
// !!DO NOT EDIT THIS COMMENT DIRECTLY!!! (edit ./q-component.md instead)
Expand Down Expand Up @@ -249,6 +251,7 @@ export function qComponent<PROPS = {}, STATE = {}>({
[AttributeMarker.OnHydrate]: onHydrate,
[AttributeMarker.OnDehydrate]: onDehydrate,
[AttributeMarker.ComponentStyles]: styles,
[AttributeMarker.ComponentUnscopedStyles]: unscopedStyles,
...props,
...jsxProps,
});
Expand All @@ -261,8 +264,9 @@ export function qComponent<PROPS = {}, STATE = {}>({
QComponent.onHydrate = onHydrate || null;
QComponent.onDehydrate = onDehydrate || null;
QComponent.styles = styles || null;
QComponent.styleHostClass = styleHost(styles) || null;
QComponent.styleClass = styleContent(styles) || null;
const styleId = styleKey(styles);
QComponent.styleHostClass = styleHost(styleId) || null;
QComponent.styleClass = styleContent(styleId) || null;
return QComponent;
}

Expand Down Expand Up @@ -333,7 +337,7 @@ export interface QComponent<PROPS extends {} = any, STATE extends {} = any>
onUnmount: QHook<PROPS, STATE, undefined, void> | null;
onDehydrate: QHook<PROPS, STATE, undefined, void> | null;
onHydrate: QHook<PROPS, STATE, undefined, void> | null;
styles: QrlStyles<any> | null;
styles: QStyles<any> | null;
styleClass: string | null;
styleHostClass: string | null;
props: Record<string, any>;
Expand Down
6 changes: 3 additions & 3 deletions src/core/component/q-component.unit.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Fragment, h, qHook, qObject } from '@builder.io/qwik';
import { ElementFixture, trigger } from '../../testing/element_fixture';
import { expectDOM } from '../../testing/expect-dom';
import { expectDOM } from '../../testing/expect-dom.unit';
import { qRender } from '../render/q-render.public';
import { TEST_CONFIG } from '../util/test_config';
import { qComponent } from './q-component.public';
import { qrlStyles } from './qrl-styles';
import { qStyles } from './qrl-styles';

describe('q-component', () => {
it('should declare and render basic component', async () => {
Expand Down Expand Up @@ -90,7 +90,7 @@ export const HelloWorld = qComponent({
onRender: qHook(() => {
return <span>Hello World</span>;
}),
styles: qrlStyles('./mock.unit.css#ABC123'),
styles: qStyles('./mock.unit.css#ABC123'),
});

/////////////////////////////////////////////////////////////////////////////
Expand Down
28 changes: 14 additions & 14 deletions src/core/component/qrl-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,48 @@ import type { QRL } from '../import/qrl';
import { toDevModeQRL } from '../import/qrl-test';
import { AttributeMarker } from '../util/markers';
import { qTest } from '../util/qdev';
import { hashCode } from '../util/hash_code';
import type { QComponent } from './q-component.public';

/**
* @public
*/
export interface QrlStyles<COMP extends QComponent> extends QRL<COMP> {
export interface QStyles<COMP extends QComponent> extends QRL<COMP> {
__brand__qrl__styles__: 'QrlStyles';
__types__: COMP;
}

/**
* @public
*/
// TODO(misko): Rename to qrlComponentStyles to be consistent with qrlOnRender????
export function qrlStyles<COMP extends QComponent>(styles: string): QrlStyles<COMP> {
export function qStyles<COMP extends QComponent>(styles: string): QStyles<COMP> {
if (qTest) {
return String(toDevModeQRL(styles, new Error())) as any;
}
return styles as unknown as QrlStyles<COMP>;
return styles as unknown as QStyles<COMP>;
}

/**
* @public
*/
export function styleKey(qrl: QrlStyles<any> | undefined): string | undefined {
return qrl && String(qrl).split('#')[1];
export function styleKey(qStyles: QStyles<any> | undefined): string | undefined {
return qStyles && String(hashCode(String(qStyles)));
}

/**
* @public
*/
export function styleHost(qrl: QrlStyles<any>): string;
export function styleHost(qrl: QrlStyles<any> | undefined): string | undefined;
export function styleHost(qrl: QrlStyles<any> | undefined): string | undefined {
return qrl && AttributeMarker.ComponentStylesPrefixHost + styleKey(qrl);
export function styleHost(styleId: string): string;
export function styleHost(styleId: string | undefined): string | undefined;
export function styleHost(styleId: string | undefined): string | undefined {
return styleId && AttributeMarker.ComponentStylesPrefixHost + styleId;
}

/**
* @public
*/
export function styleContent(qrl: QrlStyles<any>): string;
export function styleContent(qrl: QrlStyles<any> | undefined): string | undefined;
export function styleContent(qrl: QrlStyles<any> | undefined): string | undefined {
return qrl && AttributeMarker.ComponentStylesPrefixContent + styleKey(qrl);
export function styleContent(styleId: string): string;
export function styleContent(styleId: string | undefined): string | undefined;
export function styleContent(styleId: string | undefined): string | undefined {
return styleId && AttributeMarker.ComponentStylesPrefixContent + styleId;
}
2 changes: 1 addition & 1 deletion src/core/render/cursor.unit.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Fragment, h, Slot } from '@builder.io/qwik';
import { expectDOM } from '../../testing/expect-dom';
import { expectDOM } from '../../testing/expect-dom.unit';
import { toDOM } from '../../testing/jsx';
import { qHook } from '../component/qrl-hook.public';
import { AttributeMarker } from '../util/markers';
Expand Down
4 changes: 3 additions & 1 deletion src/core/render/jsx/types/jsx-qwik-attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,6 @@ interface QwikDOMEvents {
'on:wheel'?: QRL;
}

export interface DOMAttributes<T> extends QwikProps, QwikGlobalEvents, QwikDOMEvents {}
export interface DOMAttributes<T> extends QwikProps, QwikGlobalEvents, QwikDOMEvents {
children?: any;
}
13 changes: 9 additions & 4 deletions src/core/render/q-render.unit.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Fragment, h, Host } from '@builder.io/qwik';
import { ElementFixture, trigger } from '../../testing/element_fixture';
import { expectDOM } from '../../testing/expect-dom';
import { expectDOM } from '../../testing/expect-dom.unit';
import { qComponent } from '../component/q-component.public';
import { qHook } from '../component/qrl-hook.public';
import { qrlStyles } from '../component/qrl-styles';
import { qStyles, styleKey } from '../component/qrl-styles';
import { TEST_CONFIG } from '../util/test_config';
import { Async, JSXPromise, PromiseValue } from './jsx/async.public';
import { Slot } from './jsx/slot.public';
Expand Down Expand Up @@ -304,14 +304,18 @@ describe('q-render', () => {
expectRendered(
<hello-world
on:q-render={HelloWorld.onRender}
q:style={HelloWorld.styles as any}
q:sstyle={HelloWorld_styles}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is q:sstyle correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scoped Style. Not loving it, but for now until we figure out something better.

class={HelloWorld.styleHostClass as any}
>
<span class={HelloWorld.styleClass as any}>
{'Hello'} {'World'}
</span>
</hello-world>
);
const style = fixture.document.querySelector(
'style[q\\:style="' + styleKey(HelloWorld_styles) + '"]'
)!;
expect(style.textContent).toEqual('span { color: red; }');
});
});
});
Expand All @@ -323,9 +327,10 @@ describe('q-render', () => {
//////////////////////////////////////////////////////////////////////////////////////////
// Hello World
//////////////////////////////////////////////////////////////////////////////////////////
const HelloWorld_styles = qStyles<any>(`span { color: red; }`);
export const HelloWorld = qComponent<{ name?: string }, { salutation: string }>({
tagName: 'hello-world',
styles: qrlStyles<any>('./mock.unit.css#ABC123'),
styles: HelloWorld_styles,
onMount: qHook(() => ({ salutation: 'Hello' })),
onRender: qHook((props, state) => {
return (
Expand Down
10 changes: 10 additions & 0 deletions src/core/util/hash_code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

export function hashCode(text: string, hash: number = 0) {
if (text.length === 0) return hash;
for (let i = 0; i < text.length; i++) {
const chr = text.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return Number(Math.abs(hash)).toString(36);
}
7 changes: 6 additions & 1 deletion src/core/util/markers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ export const enum AttributeMarker {
/**
* Component Styles.
*/
ComponentStyles = 'q:style',
ComponentStyles = 'q:sstyle',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok guess it's correct, but why the change from q:style to q:sstyle?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we can have both scoped and unscoped styles.


/**
* Unscoped Component Styles.
*/
ComponentUnscopedStyles = 'q:ustyle',

/**
* Component style host prefix
Expand Down
Loading