add react support into lib

pull/580/head
Cory LaViska 2021-11-04 07:27:18 -04:00
rodzic c88ea6666b
commit a4c9b9c8cf
20 zmienionych plików z 652 dodań i 237 usunięć

1
.gitignore vendored
Wyświetl plik

@ -5,3 +5,4 @@ docs/search.json
dist
examples
node_modules
src/react

Wyświetl plik

@ -5,6 +5,11 @@
- [Themes](/getting-started/themes)
- [Customizing](/getting-started/customizing)
- Frameworks
- [React](/frameworks/react)
- [Vue](/frameworks/vue)
- [Angular](/frameworks/angular)
- Resources
- [Community](/resources/community)
- [Contributing](/resources/contributing)

Wyświetl plik

@ -61,11 +61,17 @@
border: solid 1px rgb(var(--sl-color-neutral-200));
border-bottom: none;
border-radius: 0 !important;
margin: 0;
display: none;
}
.code-block--expanded .code-block__source {
.code-block__source pre {
margin: 0;
}
.code-block--expanded.code-block--show-html .code-block__source--html,
.code-block--expanded.code-block--show-react .code-block__source--react,
/* When a code block is expanded but doesn't have a React example, force the HTML example to show */
.code-block--expanded:not(.code-block--has-react) .code-block__source--html {
display: block;
}
@ -97,6 +103,19 @@
border-right: solid 1px rgb(var(--sl-color-neutral-200));
}
.code-block__button--html,
.code-block__button--react {
width: 70px;
display: flex;
place-items: center;
justify-content: center;
}
.code-block__button--selected {
font-weight: 700;
color: rgb(var(--sl-color-primary-600));
}
.code-block__button--codepen {
display: flex;
place-items: center;

Wyświetl plik

@ -1,10 +1,30 @@
(() => {
const version = sessionStorage.getItem('sl-version');
let flavor = localStorage.getItem('flavor') || 'html'; // "html" or "react"
let count = 1;
if (!window.$docsify) {
throw new Error('Docsify must be loaded before installing this plugin.');
}
function convertModuleLinks(html) {
return html.replace(/@shoelace-style\/shoelace/g, `https://cdn.skypack.dev/@shoelace-style/shoelace@${version}`);
}
function getAdjacentExample(name, pre) {
let currentPre = pre.nextElementSibling;
while (currentPre?.tagName.toLowerCase() === 'pre') {
if (currentPre?.getAttribute('data-lang').split(' ').includes(name)) {
return currentPre;
}
currentPre = currentPre.nextElementSibling;
}
return null;
}
function runScript(script) {
const newScript = document.createElement('script');
@ -28,36 +48,66 @@
hook.afterEach(function (html, next) {
const domParser = new DOMParser();
const doc = domParser.parseFromString(html, 'text/html');
const codePenButton = `
<button type="button" class="code-block__button code-block__button--codepen" title="Edit on CodePen">
<svg
width="138"
height="26"
viewBox="0 0 138 26"
fill="none"
stroke="currentColor"
stroke-width="2.3"
stroke-linecap="round"
stroke-linejoin="round"
const htmlButton = `
<button
type="button"
title="Show HTML code"
class="code-block__button code-block__button--html ${flavor === 'html' ? 'code-block__button--selected' : ''}"
>
<path d="M80 6h-9v14h9 M114 6h-9 v14h9 M111 13h-6 M77 13h-6 M122 20V6l11 14V6 M22 16.7L33 24l11-7.3V9.3L33 2L22 9.3V16.7z M44 16.7L33 9.3l-11 7.4 M22 9.3l11 7.3 l11-7.3 M33 2v7.3 M33 16.7V24 M88 14h6c2.2 0 4-1.8 4-4s-1.8-4-4-4h-6v14 M15 8c-1.3-1.3-3-2-5-2c-4 0-7 3-7 7s3 7 7 7 c2 0 3.7-0.8 5-2 M64 13c0 4-3 7-7 7h-5V6h5C61 6 64 9 64 13z" />
</svg>
</button>
`;
HTML
</button>
`;
const reactButton = `
<button
type="button"
title="Show React code"
class="code-block__button code-block__button--react ${
flavor === 'react' ? 'code-block__button--selected' : ''
}"
>
React
</button>
`;
const codePenButton = `
<button type="button" class="code-block__button code-block__button--codepen" title="Edit on CodePen">
<svg
width="138"
height="26"
viewBox="0 0 138 26"
fill="none"
stroke="currentColor"
stroke-width="2.3"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M80 6h-9v14h9 M114 6h-9 v14h9 M111 13h-6 M77 13h-6 M122 20V6l11 14V6 M22 16.7L33 24l11-7.3V9.3L33 2L22 9.3V16.7z M44 16.7L33 9.3l-11 7.4 M22 9.3l11 7.3 l11-7.3 M33 2v7.3 M33 16.7V24 M88 14h6c2.2 0 4-1.8 4-4s-1.8-4-4-4h-6v14 M15 8c-1.3-1.3-3-2-5-2c-4 0-7 3-7 7s3 7 7 7 c2 0 3.7-0.8 5-2 M64 13c0 4-3 7-7 7h-5V6h5C61 6 64 9 64 13z" />
</svg>
</button>
`;
[...doc.querySelectorAll('code[class^="lang-"]')].map(code => {
if (code.classList.contains('preview')) {
const pre = code.closest('pre');
const preId = `code-block-preview-${count}`;
const toggleId = `code-block-toggle-${count}`;
const reactPre = getAdjacentExample('react', pre);
const hasReact = reactPre !== null;
const examples = [
{
name: 'HTML',
codeBlock: pre
}
];
pre.id = preId;
pre.classList.add('code-block__source');
pre.setAttribute('data-lang', pre.getAttribute('data-lang').replace(/ preview$/, ''));
pre.setAttribute('aria-labelledby', toggleId);
const codeBlock = `
<div class="code-block">
<div class="code-block code-block--show-${flavor} ${hasReact ? 'code-block--has-react' : ''}">
<div class="code-block__preview">
${code.textContent}
<div class="code-block__resizer">
@ -65,9 +115,23 @@
</div>
</div>
${pre.outerHTML}
<div class="code-block__source code-block__source--html">
${pre.outerHTML}
</div>
${
hasReact
? `
<div class="code-block__source code-block__source--react">
${reactPre.outerHTML}
</div>
`
: ''
}
<div class="code-block__buttons">
${hasReact ? ` ${htmlButton} ${reactButton} ` : ''}
<button type="button" class="code-block__button code-block__toggle" aria-expanded="false" aria-controls="${preId}">
View Source
<svg
@ -88,11 +152,15 @@
`;
pre.replaceWith(domParser.parseFromString(codeBlock, 'text/html').body);
if (reactPre) reactPre.remove();
count++;
}
});
// Force the highlighter to run again so JSX fields get highlighted properly
requestAnimationFrame(() => Prism.highlightAll());
next(doc.body.innerHTML);
});
@ -139,49 +207,35 @@
});
});
// Open in CodePen
// Toggle source mode
document.addEventListener('click', event => {
const button = event.target.closest('button');
const codeBlock = button?.closest('.code-block');
if (button?.classList.contains('code-block__button--codepen')) {
const codeBlock = button.closest('.code-block');
const html = codeBlock.querySelector('.code-block__source > code').textContent;
const version = sessionStorage.getItem('sl-version');
const form = document.createElement('form');
form.action = 'https://codepen.io/pen/define';
form.method = 'POST';
form.target = '_blank';
// Docs: https://blog.codepen.io/documentation/prefill/
const data = {
title: '',
description: '',
tags: ['shoelace', 'web components'],
editors: '100',
head: `<meta name="viewport" content="width=device-width">`,
css_external: ``,
js_external: ``,
js_module: true,
html:
`<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${version}/dist/themes/light.css">\n` +
`<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${version}/dist/shoelace.js"></script>\n` +
`\n` +
html,
css: `body {\n font: 16px sans-serif;\n}`,
js: ``
};
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'data';
input.value = JSON.stringify(data);
form.append(input);
document.body.append(form);
form.submit();
form.remove();
if (button?.classList.contains('code-block__button--html')) {
flavor = 'html';
} else if (button?.classList.contains('code-block__button--react')) {
flavor = 'react';
} else {
return;
}
localStorage.setItem('flavor', flavor);
[...document.querySelectorAll('.code-block')].map(codeBlock => {
codeBlock.classList.toggle('code-block--show-html', flavor === 'html');
codeBlock.classList.toggle('code-block--show-react', flavor === 'react');
codeBlock
.querySelector('.code-block__button--html')
?.classList.toggle('code-block__button--selected', flavor === 'html');
codeBlock
.querySelector('.code-block__button--react')
?.classList.toggle('code-block__button--selected', flavor === 'react');
});
// Expand the code block
codeBlock.classList.add('code-block--expanded');
codeBlock.querySelector('.code-block__toggle').setAttribute('aria-expanded', 'true');
});
// Expand and collapse code blocks
@ -205,4 +259,80 @@
});
}
});
// Open in CodePen
document.addEventListener('click', event => {
const button = event.target.closest('button');
if (button?.classList.contains('code-block__button--codepen')) {
const codeBlock = button.closest('.code-block');
const htmlExample = codeBlock.querySelector('.code-block__source--html > pre > code')?.textContent;
const reactExample = codeBlock.querySelector('.code-block__source--react > pre > code')?.textContent;
const isReact = flavor === 'react' && typeof reactExample === 'string';
const editors = isReact ? '0010' : '1000';
let htmlTemplate = '';
let jsTemplate = '';
let cssTemplate = '';
const form = document.createElement('form');
form.action = 'https://codepen.io/pen/define';
form.method = 'POST';
form.target = '_blank';
// HTML templates
if (!isReact) {
htmlTemplate =
`<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${version}/dist/shoelace.js"></script>\n` +
'\n' +
htmlExample;
jsTemplate = '';
}
// React templates
if (isReact) {
htmlTemplate = '<div id="root"></div>';
jsTemplate =
`import React, { useRef } from 'https://cdn.skypack.dev/react@17.0.2';\n` +
`import ReactDOM from 'https://cdn.skypack.dev/react-dom@17.0.2';\n` +
convertModuleLinks(reactExample) +
'\n' +
'\n' +
'ReactDOM.render(<App />, document.getElementById("root"));';
}
// CSS templates
cssTemplate =
`@import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${version}/dist/themes/light.css';\n` +
'\n' +
'body {\n' +
' font: 16px sans-serif;\n' +
'}';
// Docs: https://blog.codepen.io/documentation/prefill/
const data = {
title: '',
description: '',
tags: ['shoelace', 'web components'],
editors,
head: `<meta name="viewport" content="width=device-width">`,
css_external: ``,
js_external: ``,
js_module: true,
js_pre_processor: isReact ? 'babel' : 'none',
html: htmlTemplate,
css: cssTemplate,
js: jsTemplate
};
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'data';
input.value = JSON.stringify(data);
form.append(input);
document.body.append(form);
form.submit();
form.remove();
}
});
})();

Wyświetl plik

@ -392,6 +392,41 @@
return prop.kind === 'field' && prop.privacy !== 'private';
});
if (component.path) {
/* prettier-ignore */
result += `
## Importing
<sl-tab-group>
<sl-tab slot="nav" panel="cdn" active>CDN</sl-tab>
<sl-tab slot="nav" panel="bundler">Bundler</sl-tab>
<sl-tab slot="nav" panel="react">React</sl-tab>
<sl-tab-panel name="cdn">\n
To cherry pick this component from [the CDN](https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace):
\`\`\`js
import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${metadata.package.version}/${component.path}';
\`\`\`
</sl-tab-panel>
<sl-tab-panel name="bundler">\n
To import this component using [a bundler](/getting-started/installation#bundling):
\`\`\`js
import '@shoelace-style/shoelace/${component.path}';
\`\`\`
</sl-tab-panel>
<sl-tab-panel name="react">\n
To import this component as a [React component](/frameworks/react):
\`\`\`js
import { ${component.name} } from '@shoelace-style/shoelace/dist/react';
\`\`\`
</sl-tab-panel>
</sl-tab-group>
`;
}
if (props?.length) {
result += `
## Properties
@ -453,41 +488,6 @@
`;
}
if (component.path) {
/* prettier-ignore */
result += `
## Importing
<sl-tab-group>
<sl-tab slot="nav" panel="cdn" active>CDN</sl-tab>
<sl-tab slot="nav" panel="bundler">Bundler</sl-tab>
<sl-tab slot="nav" panel="react">React</sl-tab>
<sl-tab-panel name="cdn">\n
To import this component from [the CDN](https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace):
\`\`\`js
import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${metadata.package.version}/${component.path}';
\`\`\`
</sl-tab-panel>
<sl-tab-panel name="bundler">\n
To import this component using [a bundler](/getting-started/installation#bundling):
\`\`\`js
import '@shoelace-style/shoelace/${component.path}';
\`\`\`
</sl-tab-panel>
<sl-tab-panel name="react">\n
To import this component using [\`@shoelace-style/react\`](https://www.npmjs.com/package/@shoelace-style/react):
\`\`\`js
import { ${component.name} } from '@shoelace-style/react';
\`\`\`
</sl-tab-panel>
</sl-tab-group>
`;
}
// Strip whitespace so markdown doesn't process things as code blocks
return result.replace(/^ +| +$/gm, '');
});

Wyświetl plik

@ -197,7 +197,6 @@ strong {
.markdown-section {
max-width: 860px;
overflow-anchor: none;
}
.anchor span {

Wyświetl plik

@ -0,0 +1,22 @@
# Angular
## Installation
Angular [plays nice](https://custom-elements-everywhere.com/#angular) with custom elements. Just make sure to apply the custom elements schema as shown below.
```js
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [],
bootstrap: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule {}
```
?> Are you using Shoelace with Angular? This page could use some love. [Please help us improve it!](https://github.com/shoelace-style/shoelace/blob/next/docs/frameworks/angular.md)

Wyświetl plik

@ -0,0 +1,88 @@
# React
React [doesn't play nice](https://custom-elements-everywhere.com/#react) with custom elements, so Shoelace offers a React version of every component.
## Installation
To add Shoelace to your React app, install the package from npm.
```bash
npm install @shoelace-style/shoelace
```
Nest, [include a theme](/getting-started/themes) and set the [base path](/getting-started/installation#setting-the-base-path) for icons and other assets. In this example, we'll import the light theme and use the CDN as a base path.
```jsx
// App.jsx
import '@shoelace-style/shoelace/dist/themes/light.css';
import { setBasePath } from '@shoelace-style/shoelace/dist/utiltiies/base-path';
setBasePath('https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/');
```
?> If you'd rather not use the CDN for assets, you can create a [copy task](https://webpack.js.org/plugins/copy-webpack-plugin/) to copy `node_modules/@shoelace-style/shoelace/dist/assets` into your app's `public` directory. Then you can point the base path to that folder instead.
Now you can start using components!
## Importing Components
Every Shoelace component is available to import as a React component. Note that we're importing the `<SlButton>` _React component_ instead of the `<sl-button>` _custom element_ in the example below.
```jsx
import { SlButton } from '@shoelace-style/shoelace/dist/react';
const MyComponent = () => (
<SlButton type="primary">
Click me
</SlButton>
);
export default MyComponent;
```
You can find a copy + paste import for every component at the bottom of each page in the documentation.
## Event Handling
Many Shoelace components emit [custom events](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent). For example, the [input component](/components/input) emits the `sl-input` event when it receives input. In React, you can listen for the event using `onSlInput`.
Here's how you can bind the input's value to a state variable.
```jsx
import { useState } from 'react';
import { SlInput } from '@shoelace-style/shoelace/dist/react';
function MyComponent() {
const [value, setValue] = useState('');
return (
<SlInput
value={value}
onSlInput={event => setValue(event.target.value)}
/>
)
};
export default MyComponent;
```
If you're using TypeScript, it's important to note that `event.target` will be a reference to the underlying custom element. You can use `(event.target as any).value` as a quick fix, or you can strongly type the event target as shown below.
```tsx
import { useState } from 'react';
import { SlInput } from '@shoelace-style/shoelace/dist/react';
import type SlInputElement from '@shoelace-style/shoelace/dist/components/input/input';
function MyComponent() {
const [value, setValue] = useState('');
return (
<SlInput
value={value}
onSlInput={event => setValue((event.target as SlInputElement).value)}
/>
)
};
export default MyComponent;
```

Wyświetl plik

@ -0,0 +1,71 @@
# Vue
Vue [plays nice](https://custom-elements-everywhere.com/#vue) with custom elements, so you can use Shoelace in your Vue apps with ease.
## Installation
To add Shoelace to your Vue app, install the package from npm.
```bash
npm install @shoelace-style/shoelace
```
You'll need to tell Vue to ignore Shoelace components. This is pretty easy because they all start with `sl-`.
```js
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
app.config.compilerOptions.isCustomElement = tag => tag.startsWith('sl-');
app.mount('#app');
```
## Binding Complex Data
When binding complex data such as objects and arrays, use the `.prop` modifier to make Vue bind them as a property instead of an attribute.
```html
<sl-color-picker :swatches.prop="mySwatches" />
```
## Two-way Binding
One caveat is there's currently [no support for v-model on custom elements](https://github.com/vuejs/vue/issues/7830), but you can still achieve two-way binding manually.
```html
<!-- This doesn't work -->
<sl-input v-model="name">
<!-- This works, but it's a bit longer -->
<sl-input :value="name" @input="name = $event.target.value">
```
If that's too verbose for your liking, you can use a custom directive instead. [This utility](https://www.npmjs.com/package/@shoelace-style/vue-sl-model) adds a custom directive that will work just like `v-model` but for Shoelace components. To install it, use this command.
```bash
npm install @shoelace-style/vue-sl-model
```
Next, import the directive and enable it like this.
```js
import ShoelaceModelDirective from '@shoelace-style/vue-sl-model';
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
app.use(ShoelaceModelDirective);
app.config.compilerOptions.isCustomElement = tag => tag.startsWith('sl-');
app.mount('#app');
```
Now you can use the `v-sl-model` directive to keep your data in sync!
```html
<sl-input v-sl-model="name">
```

Wyświetl plik

@ -2,7 +2,9 @@
You can use Shoelace via CDN or by installing it locally. You can also [cherry pick](#cherry-picking) individual components for faster load times.
## CDN Installation (Recommended)
If you're using a framework, make sure to check out the pages for [React](/frameworks/react), [Vue](/frameworks/vue), and [Angular](/frameworks/angular).
## CDN Installation (Easiest)
The easiest way to install Shoelace is with the CDN. Just add the following tags to your page to get all components and the default light theme.
@ -11,6 +13,8 @@ The easiest way to install Shoelace is with the CDN. Just add the following tags
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace.js"></script>
```
?> If you're only using a handful of components, it will be more efficient to [cherry pick](#cherry-picking) the ones you need.
### Dark Theme
If you prefer to use the dark theme instead, use this. Note the `sl-theme-dark` class on the `<html>` element. [Learn more about the Dark Theme.](/getting-started/themes#dark-theme)
@ -77,7 +81,7 @@ However, if you're [cherry picking](#cherry-picking) or [bundling](#bundling) Sh
## Cherry Picking
The previous approach is the _easiest_ way to load Shoelace, but easy isn't always efficient. You'll incur the full size of the library even if you only use a handful of components. This is convenient for prototyping, but may result in longer load times in production. To improve this, you can cherry pick the components you need.
The previous approach is the _easiest_ way to load Shoelace, but easy isn't always efficient. You'll incur the full size of the library even if you only use a handful of components. This is convenient for prototyping or if you're using most of the components, but it may result in longer load times in production. To improve this, you can cherry pick the components you need.
Cherry picking can be done from your local install or [directly from the CDN](https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/). This will limit the number of files the browser has to download and reduce the amount of bytes being transferred. The disadvantage is that you need to load component manually.

Wyświetl plik

@ -9,7 +9,7 @@
- Fully customizable with CSS 🎨
- Includes a dark theme 🌛
- Built with accessibility in mind ♿️
- First-party [React wrappers](/getting-started/usage#react)
- First-party [React components](/frameworks/react)
- Open source 😸
Designed in New Hampshire by [Cory LaViska](https://twitter.com/claviska).
@ -41,7 +41,8 @@ Now you have access to all of Shoelace's components! Try adding a button:
<sl-button>Click me</sl-button>
```
See the [installation instructions](getting-started/installation) for more details and other ways to install Shoelace.
This will load all of Shoelace's components, but you should probably only load the ones you're actually using. To learn how, or for other ways to install Shoelace, refer to the [installation instructions](getting-started/installation).
## New to Web Components?

Wyświetl plik

@ -1,10 +1,10 @@
# Usage
Shoelace components are just regular HTML elements, or "custom elements" to be precise. You can use them like any other element. Each component has detailed documentation that describes its full API, including properties, events, methods, and more.
Shoelace components are just regular HTML elements, or [custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements) to be precise. You can use them like any other element. Each component has detailed documentation that describes its full API, including properties, events, methods, and more.
## Web Component Basics
If you're new to custom elements, often referred to as "web components," this section will familiarize you with how to use them.
### Properties
## Properties
Many components have properties that can be set using attributes. For example, buttons accept a `size` attribute that maps to the `size` property which dictates the button's size.
@ -31,7 +31,7 @@ In rare cases, a property may require an array, an object, or a function. For ex
Refer to a component's documentation for a complete list of its properties.
### Events
## Events
You can listen for standard events such as `click`, `mouseover`, etc. as you normally would. In addition, some components emit custom events. These work the same way as standard events, but are prefixed with `sl-` to prevent collisions with standard events and other libraries.
@ -48,7 +48,7 @@ You can listen for standard events such as `click`, `mouseover`, etc. as you nor
Refer to a component's documentation for a complete list of its custom events.
### Methods
## Methods
Some components have methods you can call to trigger various behaviors. For example, you can set focus on a Shoelace input using the `focus()` method.
@ -63,7 +63,7 @@ Some components have methods you can call to trigger various behaviors. For exam
Refer to a component's documentation for a complete list of its methods and their arguments.
### Slots
## Slots
Many components use slots to accept content inside of them. The most common slot is the _default_ slot, which includes any content inside the component that doesn't have a `slot` attribute.
@ -86,7 +86,7 @@ The location of a named slot doesn't matter. You can put it anywhere inside the
Refer to a component's documentation for a complete list of available slots.
### Don't Use Self-closing Tags
## Don't Use Self-closing Tags
Custom elements cannot have self-closing tags. Similar to `<script>` and `<textarea>`, you must always include the full closing tag.
@ -98,7 +98,7 @@ Custom elements cannot have self-closing tags. Similar to `<script>` and `<texta
<sl-input></sl-input>
```
### Differences from Native Elements
## Differences from Native Elements
You might expect similarly named elements to share the same API as native HTML elements. This is not always the case. Shoelace components **are not** designed to be one-to-one replacements for their HTML counterparts.
@ -128,123 +128,3 @@ If `settings.json` already exists, simply add the above line to the root of the
### Other Editors
Most popular editors support custom code completion with a bit of configuration. Please [submit a feature request](https://github.com/shoelace-style/shoelace/issues/new/choose) for your editor of choice. PRs are also welcome!
## React
React [doesn't play nice](https://custom-elements-everywhere.com/#react) with custom elements — it's a bit finicky about props.
> React passes all data to Custom Elements in the form of HTML attributes. For primitive data this is fine, but the system breaks down when passing rich data, like objects or arrays. In these instances you end up with stringified values like `some-attr="[object Object]"` which can't actually be used.
Event handling can also be cumbersome.
> Because React implements its own synthetic event system, it cannot listen for DOM events coming from Custom Elements without the use of a workaround. Developers will need to reference their Custom Elements using a ref and manually attach event listeners with addEventListener. This makes working with Custom Elements cumbersome.
Fortunately, there's a package called [@shoelace-style/react](https://www.npmjs.com/package/@shoelace-style/react) that will let you use Shoelace components as if they were React components. You can install it using this command.
```bash
npm install @shoelace-style/react
```
Include the default theme and any components you want to use in your app.
```jsx
import '@shoelace-style/shoelace/dist/themes/light.css';
import SlButton from '@shoelace-style/react/dist/button';
// ...
const MyComponent = (props) => {
return (
<SlButton type="primary">
Click me
</SlButton>
)
};
```
## Vue
Vue [plays nice](https://custom-elements-everywhere.com/#vue) with custom elements. You just have to tell it to ignore Shoelace components. This is pretty easy because they all start with `sl-`.
```js
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
app.config.compilerOptions.isCustomElement = tag => tag.startsWith('sl-');
app.mount('#app');
```
### Binding Complex Data
When binding complex data such as objects and arrays, use the `.prop` modifier to make Vue bind them as a property instead of an attribute.
```html
<sl-color-picker :swatches.prop="mySwatches" />
```
### Two-way Binding
One caveat is there's currently [no support for v-model on custom elements](https://github.com/vuejs/vue/issues/7830), but you can still achieve two-way binding manually.
```html
<!-- This doesn't work -->
<sl-input v-model="name">
<!-- This works, but it's a bit longer -->
<sl-input :value="name" @input="name = $event.target.value">
```
If that's too verbose, you can use a custom directive instead.
### Using a Custom Directive
You can use [this utility](https://www.npmjs.com/package/@shoelace-style/vue-sl-model) to add a custom directive that will work just like `v-model` but for Shoelace components. To install it, use this command.
```bash
npm install @shoelace-style/vue-sl-model
```
Next, import the directive and enable it like this.
```js
import ShoelaceModelDirective from '@shoelace-style/vue-sl-model';
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
app.use(ShoelaceModelDirective);
app.config.compilerOptions.isCustomElement = tag => tag.startsWith('sl-');
app.mount('#app');
```
Now you can use the `v-sl-model` directive to keep your data in sync!
```html
<sl-input v-sl-model="name">
```
## Angular
Angular [plays nice](https://custom-elements-everywhere.com/#angular) with custom elements. Just make sure to apply the custom elements schema as shown below.
```js
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [],
bootstrap: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule {}
```

Wyświetl plik

@ -44,7 +44,7 @@
<link rel="stylesheet" href="/dist/themes/dark.css" />
<script type="module" src="/dist/shoelace.js"></script>
</head>
<body>
<body data-shoelace="/dist">
<div id="app"></div>
<script>
// Set the initial theme to prevent flashing
@ -88,8 +88,9 @@
<script src="https://cdn.jsdelivr.net/npm/docsify@4/lib/plugins/ga.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/docsify-copy-code@2"></script>
<script src="https://cdn.jsdelivr.net/npm/docsify-pagination@2/dist/docsify-pagination.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.19.0/components/prism-bash.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.19.0/components/prism-jsx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.25.0/components/prism-bash.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.25.0/components/prism-jsx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.25.0/components/prism-tsx.min.js"></script>
<script src="/assets/plugins/code-block/code-block.js"></script>
<script src="/assets/plugins/metadata/metadata.js"></script>
<script src="/assets/plugins/scroll-position/scroll-position.js"></script>

Wyświetl plik

@ -6,8 +6,9 @@ Components with the <sl-badge type="warning" pill>Experimental</sl-badge> badge
_During the beta period, these restrictions may be relaxed in the event of a mission-critical bug._ 🐛
## Next
## 2.0.0-beta.59
- Added React wrappers as first-class citizens
- Added eye dropper to `<sl-color-picker>` when the browser supports the [EyeDropper API](https://wicg.github.io/eyedropper-api/)
- Fixed a bug in `<sl-button-group>` where buttons groups with only one button would have an incorrect border radius
- Improved the `<sl-color-picker>` trigger's border in dark mode

Wyświetl plik

@ -14,7 +14,7 @@ This integration has been tested with the following:
To get started using Shoelace with NextJS, the following packages must be installed.
```bash
yarn add @shoelace-style/shoelace @shoelace-style/react-wrapper copy-webpack-plugin next-compose-plugins next-transpile-modules
yarn add @shoelace-style/shoelace @shoelace-style/shoelace copy-webpack-plugin next-compose-plugins next-transpile-modules
```
### Importing the Default Theme

115
package-lock.json wygenerowano
Wyświetl plik

@ -9,6 +9,7 @@
"version": "2.0.0-beta.58",
"license": "MIT",
"dependencies": {
"@lit-labs/react": "^1.0.1",
"@popperjs/core": "^2.7.0",
"@shoelace-style/animations": "^1.1.0",
"color": "^3.1.3",
@ -18,6 +19,7 @@
"@custom-elements-manifest/analyzer": "^0.4.12",
"@open-wc/testing": "^2.5.33",
"@types/color": "^3.0.1",
"@types/react": "^17.0.33",
"@web/dev-server-esbuild": "^0.2.12",
"@web/test-runner": "^0.13.5",
"@web/test-runner-puppeteer": "^0.10.0",
@ -40,6 +42,7 @@
"mkdirp": "^0.5.5",
"plop": "^2.7.4",
"prettier": "^2.2.1",
"react": "^17.0.2",
"recursive-copy": "^2.0.11",
"sinon": "^11.1.1",
"strip-css-comments": "^4.1.0",
@ -173,6 +176,11 @@
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@lit-labs/react": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-1.0.1.tgz",
"integrity": "sha512-ShvoOB34Oj0ZkSnlWdGIWzSiEBP1MUY81nC3nAsNoWqbYMS2F/EskGzwSQj7mCKNznUCbmpB272AvSMwejm3Nw=="
},
"node_modules/@lit/reactive-element": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.0.0.tgz",
@ -740,6 +748,12 @@
"integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==",
"dev": true
},
"node_modules/@types/prop-types": {
"version": "15.7.4",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
"dev": true
},
"node_modules/@types/qs": {
"version": "6.9.6",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz",
@ -752,6 +766,17 @@
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==",
"dev": true
},
"node_modules/@types/react": {
"version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.33.tgz",
"integrity": "sha512-pLWntxXpDPaU+RTAuSGWGSEL2FRTNyRQOjSWDke/rxRg14ncsZvx8AKWMWZqvc1UOaJIAoObdZhAWvRaHFi5rw==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@ -761,6 +786,12 @@
"@types/node": "*"
}
},
"node_modules/@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"dev": true
},
"node_modules/@types/serve-static": {
"version": "1.13.9",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz",
@ -2982,6 +3013,12 @@
"node": ">=8"
}
},
"node_modules/csstype": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz",
"integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==",
"dev": true
},
"node_modules/custom-elements-manifest": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/custom-elements-manifest/-/custom-elements-manifest-1.0.0.tgz",
@ -6410,6 +6447,18 @@
"node": ">=8"
}
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lower-case": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
@ -8362,6 +8411,19 @@
"node": ">= 0.8"
}
},
"node_modules/react": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
"dev": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/read-pkg": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz",
@ -11293,6 +11355,11 @@
"@hapi/hoek": "^9.0.0"
}
},
"@lit-labs/react": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-1.0.1.tgz",
"integrity": "sha512-ShvoOB34Oj0ZkSnlWdGIWzSiEBP1MUY81nC3nAsNoWqbYMS2F/EskGzwSQj7mCKNznUCbmpB272AvSMwejm3Nw=="
},
"@lit/reactive-element": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.0.0.tgz",
@ -11843,6 +11910,12 @@
"integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==",
"dev": true
},
"@types/prop-types": {
"version": "15.7.4",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
"dev": true
},
"@types/qs": {
"version": "6.9.6",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz",
@ -11855,6 +11928,17 @@
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==",
"dev": true
},
"@types/react": {
"version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.33.tgz",
"integrity": "sha512-pLWntxXpDPaU+RTAuSGWGSEL2FRTNyRQOjSWDke/rxRg14ncsZvx8AKWMWZqvc1UOaJIAoObdZhAWvRaHFi5rw==",
"dev": true,
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@ -11864,6 +11948,12 @@
"@types/node": "*"
}
},
"@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"dev": true
},
"@types/serve-static": {
"version": "1.13.9",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz",
@ -13670,6 +13760,12 @@
}
}
},
"csstype": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz",
"integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==",
"dev": true
},
"custom-elements-manifest": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/custom-elements-manifest/-/custom-elements-manifest-1.0.0.tgz",
@ -16389,6 +16485,15 @@
}
}
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"lower-case": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
@ -17951,6 +18056,16 @@
"unpipe": "1.0.0"
}
},
"react": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
"dev": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
}
},
"read-pkg": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz",

Wyświetl plik

@ -39,6 +39,7 @@
"test:watch": "web-test-runner \"src/**/*.test.ts\" --node-resolve --puppeteer --watch"
},
"dependencies": {
"@lit-labs/react": "^1.0.1",
"@popperjs/core": "^2.7.0",
"@shoelace-style/animations": "^1.1.0",
"color": "^3.1.3",
@ -48,6 +49,7 @@
"@custom-elements-manifest/analyzer": "^0.4.12",
"@open-wc/testing": "^2.5.33",
"@types/color": "^3.0.1",
"@types/react": "^17.0.33",
"@web/dev-server-esbuild": "^0.2.12",
"@web/test-runner": "^0.13.5",
"@web/test-runner-puppeteer": "^0.10.0",
@ -70,6 +72,7 @@
"mkdirp": "^0.5.5",
"plop": "^2.7.4",
"prettier": "^2.2.1",
"react": "^17.0.2",
"recursive-copy": "^2.0.11",
"sinon": "^11.1.1",
"strip-css-comments": "^4.1.0",

Wyświetl plik

@ -30,17 +30,19 @@ mkdirp.sync(outdir);
(async () => {
try {
if (types) execSync(`tsc --project . --outdir "${outdir}"`, { stdio: 'inherit' });
execSync(`node scripts/make-metadata.js --outdir "${outdir}"`, { stdio: 'inherit' });
execSync(`node scripts/make-search.js --outdir "${outdir}"`, { stdio: 'inherit' });
execSync(`node scripts/make-react.js`, { stdio: 'inherit' });
execSync(`node scripts/make-vscode-data.js --outdir "${outdir}"`, { stdio: 'inherit' });
execSync(`node scripts/make-css.js --outdir "${outdir}"`, { stdio: 'inherit' });
execSync(`node scripts/make-icons.js --outdir "${outdir}"`, { stdio: 'inherit' });
if (types) execSync(`tsc --project . --outdir "${outdir}"`, { stdio: 'inherit' });
} catch (err) {
console.error(chalk.red(err));
process.exit(1);
}
const alwaysExternal = ['@lit-labs/react', 'react'];
const buildResult = await esbuild
.build({
format: 'esm',
@ -53,13 +55,15 @@ mkdirp.sync(outdir);
// Public utilities
...(await glob('./src/utilities/**/!(*.(style|test)).ts')),
// Theme stylesheets
...(await glob('./src/themes/**/!(*.test).ts'))
...(await glob('./src/themes/**/!(*.test).ts')),
// React wrappers
...(await glob('./src/react/**/*.ts'))
],
outdir,
chunkNames: 'chunks/[name].[hash]',
incremental: serve,
define: {
// Popper.js expects this to be set
// Popper.js requires this to be set
'process.env.NODE_ENV': '"production"'
},
bundle: true,
@ -67,7 +71,11 @@ mkdirp.sync(outdir);
// We don't bundle certain dependencies in the unbundled build. This ensures we ship bare module specifiers,
// allowing end users to better optimize when using a bundler. (Only packages that ship ESM can be external.)
//
external: bundle ? undefined : ['@popperjs/core', '@shoelace-style/animations', 'lit', 'qr-creator'],
// We never bundle React or @lit-labs/react though!
//
external: bundle
? alwaysExternal
: [...alwaysExternal, '@popperjs/core', '@shoelace-style/animations', 'lit', 'qr-creator'],
splitting: true,
plugins: []
})

Wyświetl plik

@ -0,0 +1,66 @@
import chalk from 'chalk';
import commandLineArgs from 'command-line-args';
import fs from 'fs';
import del from 'del';
import mkdirp from 'mkdirp';
import path from 'path';
import pascalCase from 'pascal-case';
import prettier from 'prettier';
import prettierConfig from '../prettier.config.cjs';
import { execSync } from 'child_process';
import { getAllComponents } from './shared.js';
const outdir = path.join('./src/react');
// Clear build directory
del.sync(outdir);
mkdirp.sync(outdir);
// Fetch component metadata
const metadata = JSON.parse(fs.readFileSync('./dist/custom-elements.json', 'utf8'));
// Wrap components
console.log('Wrapping components for React...');
const components = getAllComponents(metadata);
const index = [];
components.map(component => {
const tagWithoutPrefix = component.tagName.replace(/^sl-/, '');
const componentDir = path.join(outdir, tagWithoutPrefix);
const componentFile = path.join(componentDir, 'index.ts');
const importPath = component.modulePath.replace(/^src\//, '').replace(/\.ts$/, '');
mkdirp.sync(componentDir);
const events = (component.events || []).map(event => `${`on${pascalCase(event.name)}`}: '${event.name}'`).join(',\n');
const source = prettier.format(
`
import * as React from 'react';
import { createComponent } from '@lit-labs/react';
import Component from '../../${importPath}';
export default createComponent(
React,
'${component.tagName}',
Component,
{
${events}
}
);
`,
Object.assign(prettierConfig, {
parser: 'babel-ts'
})
);
index.push(`export { default as ${component.name} } from './${tagWithoutPrefix}';`);
fs.writeFileSync(componentFile, source, 'utf8');
});
// Generate the index file
fs.writeFileSync(path.join(outdir, 'index.ts'), index.join('\n'), 'utf8');
console.log(chalk.cyan(`\nComponents have been wrapped for React! 📦\n`));

Wyświetl plik

@ -5,9 +5,10 @@ export function getAllComponents(metadata) {
module.declarations?.map(declaration => {
if (declaration.customElement) {
const component = declaration;
const modulePath = module.path;
if (component) {
allComponents.push(component);
allComponents.push(Object.assign(component, { modulePath }));
}
}
});