diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts index aa3433a20f..368dd68b5e 100644 --- a/packages/astro/src/runtime/server/index.ts +++ b/packages/astro/src/runtime/server/index.ts @@ -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; } -function extractHydrationDirectives(inputProps: Record): ExtractedHydration { - let extracted: ExtractedHydration = { +function extractDirectives(inputProps: Record): ExtractedProps { + let extracted: ExtractedProps = { hydration: null, props: {}, }; @@ -130,6 +130,9 @@ function extractHydrationDirectives(inputProps: Record): 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) { return output; } +function serializeListValue(value: any) { + const hash: Record = {} + + 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) { let output = '\n'; for (const [key, value] of Object.entries(vars)) { diff --git a/packages/astro/test/astro-class-list.test.js b/packages/astro/test/astro-class-list.test.js new file mode 100644 index 0000000000..de86705209 --- /dev/null +++ b/packages/astro/test/astro-class-list.test.js @@ -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); + }); +}); diff --git a/packages/astro/test/fixtures/astro-class-list/src/components/Span.astro b/packages/astro/test/fixtures/astro-class-list/src/components/Span.astro new file mode 100644 index 0000000000..bfadf035c5 --- /dev/null +++ b/packages/astro/test/fixtures/astro-class-list/src/components/Span.astro @@ -0,0 +1 @@ + diff --git a/packages/astro/test/fixtures/astro-class-list/src/pages/component.astro b/packages/astro/test/fixtures/astro-class-list/src/pages/component.astro new file mode 100644 index 0000000000..e904a08992 --- /dev/null +++ b/packages/astro/test/fixtures/astro-class-list/src/pages/component.astro @@ -0,0 +1,18 @@ +--- +import Component from '../components/Span.astro' +--- + + + + + + + + + + + + + + + diff --git a/packages/astro/test/fixtures/astro-class-list/src/pages/index.astro b/packages/astro/test/fixtures/astro-class-list/src/pages/index.astro new file mode 100644 index 0000000000..a642557de5 --- /dev/null +++ b/packages/astro/test/fixtures/astro-class-list/src/pages/index.astro @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + +