0
Fork 0
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:
Jonathan Neal 2021-10-22 14:22:18 -04:00 committed by Drew Powers
parent b0e407dc4b
commit d9caef63d8
5 changed files with 112 additions and 4 deletions

View file

@ -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)) {

View 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);
});
});

View file

@ -0,0 +1 @@
<span {...Astro.props} />

View 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' ]) ]} />

View 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' ]) ]} />