mirror of
https://github.com/withastro/astro.git
synced 2024-12-30 22:03:56 -05:00
Add class:list
directive (#1612)
* Add support for class:list directive The `class:list` directive serializes an expression of css class names. For React components, `className:list` is also supported. * Remove `className` support and React tests * Add tests for the absence of omitted classes
This commit is contained in:
parent
b0e407dc4b
commit
d9caef63d8
5 changed files with 112 additions and 4 deletions
|
@ -90,7 +90,7 @@ export function createComponent(cb: AstroComponentFactory) {
|
|||
return cb;
|
||||
}
|
||||
|
||||
interface ExtractedHydration {
|
||||
interface ExtractedProps {
|
||||
hydration: {
|
||||
directive: string;
|
||||
value: string;
|
||||
|
@ -100,8 +100,8 @@ interface ExtractedHydration {
|
|||
props: Record<string | number, any>;
|
||||
}
|
||||
|
||||
function extractHydrationDirectives(inputProps: Record<string | number, any>): ExtractedHydration {
|
||||
let extracted: ExtractedHydration = {
|
||||
function extractDirectives(inputProps: Record<string | number, any>): ExtractedProps {
|
||||
let extracted: ExtractedProps = {
|
||||
hydration: null,
|
||||
props: {},
|
||||
};
|
||||
|
@ -130,6 +130,9 @@ function extractHydrationDirectives(inputProps: Record<string | number, any>): E
|
|||
break;
|
||||
}
|
||||
}
|
||||
} else if (key === 'class:list') {
|
||||
// support "class" from an expression passed into a component (#782)
|
||||
extracted.props[key.slice(0, -5)] = serializeListValue(value)
|
||||
} else {
|
||||
extracted.props[key] = value;
|
||||
}
|
||||
|
@ -199,7 +202,7 @@ export async function renderComponent(result: SSRResult, displayName: string, Co
|
|||
throw new Error(`Unable to render ${metadata.displayName} because it is ${Component}!\nDid you forget to import the component or is it possible there is a typo?`);
|
||||
}
|
||||
|
||||
const { hydration, props } = extractHydrationDirectives(_props);
|
||||
const { hydration, props } = extractDirectives(_props);
|
||||
let html = '';
|
||||
|
||||
if (hydration) {
|
||||
|
@ -287,6 +290,12 @@ export function addAttribute(value: any, key: string) {
|
|||
if (value == null || value === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// support "class" from an expression passed into an element (#782)
|
||||
if (key === 'class:list') {
|
||||
return ` ${key.slice(0, -5)}="${serializeListValue(value)}"`;
|
||||
}
|
||||
|
||||
return ` ${key}="${value}"`;
|
||||
}
|
||||
|
||||
|
@ -298,6 +307,33 @@ export function spreadAttributes(values: Record<any, any>) {
|
|||
return output;
|
||||
}
|
||||
|
||||
function serializeListValue(value: any) {
|
||||
const hash: Record<string, any> = {}
|
||||
|
||||
push(value)
|
||||
|
||||
return Object.keys(hash).join(' ');
|
||||
|
||||
function push(item: any) {
|
||||
// push individual iteratables
|
||||
if (item && typeof item.forEach === 'function') item.forEach(push)
|
||||
|
||||
// otherwise, push object value keys by truthiness
|
||||
else if (item === Object(item)) Object.keys(item).forEach(
|
||||
name => {
|
||||
if (item[name]) push(name)
|
||||
}
|
||||
)
|
||||
|
||||
// otherwise, push any other values as a string
|
||||
else if (item = item != null && String(item).trim()) item.split(/\s+/).forEach(
|
||||
(name: string) => {
|
||||
hash[name] = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function defineStyleVars(astroId: string, vars: Record<any, any>) {
|
||||
let output = '\n';
|
||||
for (const [key, value] of Object.entries(vars)) {
|
||||
|
|
38
packages/astro/test/astro-class-list.test.js
Normal file
38
packages/astro/test/astro-class-list.test.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { expect } from 'chai';
|
||||
import cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({ projectRoot: './fixtures/astro-class-list/' });
|
||||
await fixture.build();
|
||||
});
|
||||
|
||||
describe('Class List', async () => {
|
||||
it('Passes class:list attributes as expected to elements', async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('[class="test control"]')).to.have.lengthOf(1);
|
||||
expect($('[class="test expression"]')).to.have.lengthOf(1);
|
||||
expect($('[class="test true"]')).to.have.lengthOf(1);
|
||||
expect($('[class="test truthy"]')).to.have.lengthOf(1);
|
||||
expect($('[class="test set"]')).to.have.lengthOf(1);
|
||||
expect($('[class="hello goodbye world friend"]')).to.have.lengthOf(1);
|
||||
|
||||
expect($('.false, .noshow1, .noshow2, .noshow3, .noshow4')).to.have.lengthOf(0);
|
||||
});
|
||||
|
||||
it('Passes class:list attributes as expected to components', async () => {
|
||||
const html = await fixture.readFile('/component/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
expect($('[class="test control"]')).to.have.lengthOf(1);
|
||||
expect($('[class="test expression"]')).to.have.lengthOf(1);
|
||||
expect($('[class="test true"]')).to.have.lengthOf(1);
|
||||
expect($('[class="test truthy"]')).to.have.lengthOf(1);
|
||||
expect($('[class="test set"]')).to.have.lengthOf(1);
|
||||
expect($('[class="hello goodbye world friend"]')).to.have.lengthOf(1);
|
||||
});
|
||||
});
|
1
packages/astro/test/fixtures/astro-class-list/src/components/Span.astro
vendored
Normal file
1
packages/astro/test/fixtures/astro-class-list/src/components/Span.astro
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<span {...Astro.props} />
|
18
packages/astro/test/fixtures/astro-class-list/src/pages/component.astro
vendored
Normal file
18
packages/astro/test/fixtures/astro-class-list/src/pages/component.astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
import Component from '../components/Span.astro'
|
||||
---
|
||||
<Component class="test control" />
|
||||
|
||||
<!-- @note: `class:list` will not be parsed if its value is not an expression -->
|
||||
<!-- <Component class:list="test" /> -->
|
||||
|
||||
<Component class:list={'test expression'} />
|
||||
|
||||
<Component class:list={[ 'array' ]} />
|
||||
|
||||
<Component class:list={{ test: true, true: true, false: false }} />
|
||||
<Component class:list={{ test: 1, truthy: '0', noshow1: 0, noshow2: '', noshow3: null, noshow4: undefined }} />
|
||||
|
||||
<Component class:list={new Set(['test', 'set'])} />
|
||||
|
||||
<Component class:list={[ 'hello goodbye', { hello: true, world: true }, new Set([ 'hello', 'friend' ]) ]} />
|
15
packages/astro/test/fixtures/astro-class-list/src/pages/index.astro
vendored
Normal file
15
packages/astro/test/fixtures/astro-class-list/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
<span class="test control" />
|
||||
|
||||
<!-- @note: `class:list` will not be parsed if its value is not an expression -->
|
||||
<!-- <span class:list="test" /> -->
|
||||
|
||||
<span class:list={'test expression'} />
|
||||
|
||||
<span class:list={[ 'array' ]} />
|
||||
|
||||
<span class:list={{ test: true, true: true, false: false }} />
|
||||
<span class:list={{ test: 1, truthy: '0', noshow1: 0, noshow2: '', noshow3: null, noshow4: undefined }} />
|
||||
|
||||
<span class:list={new Set(['test', 'set'])} />
|
||||
|
||||
<span class:list={[ 'hello goodbye', { hello: true, world: true }, new Set([ 'hello', 'friend' ]) ]} />
|
Loading…
Reference in a new issue