├── .gitignore ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── pwa.config.js ├── react.d.ts ├── react.js ├── rollup.config.js ├── src ├── assets │ ├── favicon.png │ ├── features.json │ ├── icon.png │ ├── link.svg │ ├── manifest.json │ ├── robots.txt │ ├── shapes │ │ ├── circle.svg │ │ ├── cross.svg │ │ ├── penta.svg │ │ ├── point.svg │ │ └── square.svg │ ├── twitter_heart.png │ └── video.svg ├── components │ ├── App.svelte │ └── App │ │ ├── index.css │ │ ├── index.js │ │ ├── index.vue │ │ └── style.css ├── index.css ├── index.html └── index.js └── vue.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .DS_Store 3 | /build 4 | *.lock 5 | *.log 6 | 7 | # Editor directories and files 8 | .idea 9 | *.suo 10 | *.ntvs* 11 | *.njsproj 12 | *.sln 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # svelte-adapter 2 | 3 | A simple utility that allows you to use [Svelte](https://svelte.dev/) components inside [React](https://reactjs.org/) or [Vue](https://vuejs.org/) components. 4 | 5 | There is an adapter each for Vue and React which allows you to pass props and respond to events in a way that makes sense for that library. 6 | 7 | This isn't a perfect solution, there are some limitations. 8 | 9 | --- 10 | 11 | - [Install](#install) 12 | - [Use it](#use-it) 13 | - [Examples](#examples) 14 | - [React](#react) 15 | - [Vue](#vue) 16 | - [Limitations](#limitations) 17 | 18 | # Install 19 | 20 | With [npm](https://www.npmjs.com/): 21 | 22 | ```bash 23 | npm install svelte-adapter 24 | ``` 25 | 26 | Or with [yarn](https://yarnpkg.com/lang/en/): 27 | 28 | ```bash 29 | yarn add svelte-adapter 30 | ``` 31 | 32 | # Use it 33 | 34 | Each 'adapter' is a simple function that takes a svelte component and a few options and returns a Vue or React component that can be used in Vue templates or JSX as you would expect. The returned components will always have a wrapper element, by default this is a `` but it can be customised. 35 | 36 | The adapters both have the same signature: 37 | 38 | ```ts 39 | adapter(Component: SvelteComponent, styleObject?: object, wrapperElement?: string) : Component 40 | ``` 41 | 42 | - `Component` should be a _compiled_ svelte component, either precompiled or compiled as part of your build step using `rollup-plugin-svelte` for rollup or `svelte-loader` from webpack. 43 | - `styleObject` (optional) should be an object of styles that will be applied to the base elemement. This should be a valid JavaScript object with camelCased css property names. This defaults to an empty object. 44 | - `wrapperElement` (optional). All component have a base 'wrapper' element, by default this is a `` but you can pass in a string to customise this behaviour (eg: `'div'`, `'li'`, etc.) If need a specific wrapper element but don't care about styles, you can simply pass an empty object as the `styleObject`. 45 | 46 | In the examples below, the Svelte component we will be using is a simple component that accepts a prop that will be rendered and emits an event upon clicking a button. 47 | 48 | ```svelte 49 | 55 | 56 |

{ number }

57 | 60 | ``` 61 | 62 | ## Examples 63 | 64 | ### React 65 | 66 | [Try it on codesandbox](https://codesandbox.io/s/svelte-adapterreact-8s33k) 67 | 68 | The React-Svelte adapter is the default export from `svelte-adpater/react`. 69 | 70 | The implementation of the React adapter uses [hooks](https://reactjs.org/docs/hooks-intro.html) so you will need a recent version of React for this to work. If necessary I could add a class-based version at some stage. 71 | 72 | Using the adaptor is very straight-forward, the adapter is a Higher Order Component that takes a Svelte component and returns a React component. The below example assumes your are compiling Svelte components as they are imported using webpack or rollup. If not, just import the compiled javascript file instead. 73 | 74 | Svelte components can emit events which doesn't quite make sense in React. Any events emitted by the svelte component can be passed callbacks via an `on*` prop containing a function, this function will fire when the event is emitted. Any prop that starts with `on` followed by a capital letter is assumed to be an event and will be used to add a listener. `onClick` will fire the provided callback when `'click'` events are emitted, `onSomethingRandom` will do the same for `'somethingRandom'` events. This does not interfere with props that have the same naming convention, they will all be passed regardless. 75 | 76 | Some Svelte component's allow you to `bind` to internal data which doesn't make too much sense outside of Svelte yet they often form an important part of the API. Instead I have added the option to use a `watch*` prop (similar to the `on*` prop). This also takes a callback function and recieves the value you wish to watch as its only argument. `watchNumber={ n => setCount(n) }` would watch the internal value `number`, when `number` changes the callback you passed to it would be executed receiving the new `number` value as its only argument. 77 | 78 | This may seem strange but many Svelte components are written to make use of this `bind` syntax, without it there is often a hole in the API leaving you unable to respond to internal state changes. You will probably want to control your state with React, this `watch*` prop is an escape hatch that allows you to pull out those internal values to use however you wish. 79 | 80 | Normal props behave as you would expect. 81 | 82 | ```jsx 83 | import React, { useState } from "react"; 84 | import toReact from "svelte-adapter/react"; 85 | 86 | import SvelteApp from "../App.svelte"; 87 | 88 | const baseStyle = { 89 | width: "50%" 90 | }; 91 | 92 | const SvelteInReact = toReact(SvelteApp, baseStyle, "div"); 93 | 94 | const App = () => { 95 | const [count, setCount] = useState(10); 96 | 97 | const handleClick = () => setCount(prevCount => prevCount + 1); 98 | 99 | return ( 100 |
101 | setCount(n)} 105 | /> 106 | 107 |
108 | ); 109 | }; 110 | ``` 111 | 112 | ### Vue 113 | 114 | [Try it on codesandbox](https://codesandbox.io/s/svelte-adaptervue-40uwg) 115 | 116 | The Vue-Svelte adapter is the default export from `svelte-adpater/vue`. 117 | 118 | Using the adapter is very straight-forward, the adapter is a Higher Order Component that takes a Svelte component and returns a Vue component. The below example assumes your are compiling Svelte components as they are imported using webpack or rollup. If not, just import the compiled javascript file instead. 119 | 120 | Since Vue has an event mechanism similar to Svelte, event directives can be used as expected. `v-on:*` or `@*` directives will listen for the matching events on the Svelte component. `v-on:click` or `@click` will fire the provided callback when `'click'` events are emitted, `v-on:somethingRandom` or `@somethingrandom` will do the same for `'somethingRandom'` events. Other props behave as you would expect. 121 | 122 | Some Svelte component's allow you to `bind` to internal data which doesn't make too much sense outside of Svelte yet they often form an important part of the API. I have added the option to use a `@watch:*` prop (similar to the `@:*` prop). This also takes a callback function and recieves the value you wish to watch as its only argument. `@watch:number="setCount"` would watch the internal value `number`, when `number` changes the callback you passed to it would be executed receiving the new `number` value as its only argument. 123 | 124 | This may seem strange but many Svelte components are written to make use of this `bind` syntax, without it there is often a hole in the API leaving you unable to respond to internal state changes. You will probably want to control your state in a Vue component, this `@watch:*` prop is an escape hatch that allows you to pull out those internal values to use however you wish. 125 | 126 | Normal props behave as expected. 127 | 128 | ```vue 129 | 139 | 140 | 167 | ``` 168 | 169 | ## Limitations 170 | 171 | While everything should work in most situations, it is not currently possible to pass children or slots to Svelte components with these adapters. 172 | 173 | This won't work with any of the adpaters: 174 | 175 | ```html 176 | 177 |

Hello

178 |
179 | ``` 180 | 181 | There may be more limitations that I am unaware of, this package is really just intended a simple way to use Svelte and should work most of the time. The code is short and simple, if you have specific needs you will probably be better off writing something custom for your application. 182 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | throw new Error( 2 | "svelte-adapter: You probably meant to import from `svelte-adapter/react` or `svelte-adapter/vue`." 3 | ); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-adapter", 3 | "version": "0.5.0", 4 | "description": "Use Svelte components with Vue and React.", 5 | "author": "pngwn ", 6 | "repository": "https://github.com/pngwn/svelte-adapter", 7 | "license": "MIT", 8 | "scripts": { 9 | "test:build": "pwa export", 10 | "test:start": "sirv build -s", 11 | "test:watch": "pwa watch" 12 | }, 13 | "devDependencies": { 14 | "@pwa/cli": "latest", 15 | "@pwa/preset-react": "latest", 16 | "@pwa/preset-vue": "^0.4.1", 17 | "ganalytics": "^3.0.0", 18 | "react": "^16.5.0", 19 | "react-dom": "^16.5.0", 20 | "rollup": "^1.16.2", 21 | "sirv-cli": "^0.2.0", 22 | "svelte": "^3.20.1", 23 | "svelte-easy-crop": "^1.0.3", 24 | "svelte-loader": "^2.13.4", 25 | "vue": "^2.6.10" 26 | }, 27 | "browserslist": [ 28 | ">0.25%", 29 | "last 1 version", 30 | "not ie_mob 11", 31 | "not ie 11", 32 | "not dead" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /pwa.config.js: -------------------------------------------------------------------------------- 1 | exports.webpack = function(config, env) { 2 | let { production, webpack } = env; 3 | config.module.rules.push({ 4 | test: /\.(svelte)$/, 5 | exclude: /node_modules/, 6 | use: "svelte-loader" 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /react.d.ts: -------------------------------------------------------------------------------- 1 | import { SvelteComponent, SvelteComponentTyped } from "svelte"; 2 | import { FC } from "react"; 3 | 4 | type ExtractProps = Ctor extends new (arg: any) => SvelteComponentTyped ? Props : Record; 5 | 6 | export default function SvelteComponent>( 7 | component: CompCtor, 8 | styles?: Record, 9 | element?: string 10 | ): FC> 11 | -------------------------------------------------------------------------------- /react.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from "react"; 2 | 3 | export default (Component, style = {}, tag = "span") => props => { 4 | const container = useRef(null); 5 | const component = useRef(null); 6 | const [mounted, setMount] = useState(false); 7 | 8 | useEffect(() => { 9 | const eventRe = /on([A-Z]{1,}[a-zA-Z]*)/; 10 | const watchRe = /watch([A-Z]{1,}[a-zA-Z]*)/; 11 | 12 | component.current = new Component({ target: container.current, props }); 13 | 14 | let watchers = []; 15 | for (const key in props) { 16 | const eventMatch = key.match(eventRe); 17 | const watchMatch = key.match(watchRe); 18 | 19 | if (eventMatch && typeof props[key] === "function") { 20 | component.current.$on( 21 | `${eventMatch[1][0].toLowerCase()}${eventMatch[1].slice(1)}`, 22 | props[key] 23 | ); 24 | } 25 | 26 | if (watchMatch && typeof props[key] === "function") { 27 | watchers.push([ 28 | `${watchMatch[1][0].toLowerCase()}${watchMatch[1].slice(1)}`, 29 | props[key] 30 | ]); 31 | } 32 | } 33 | 34 | if (watchers.length) { 35 | const update = component.current.$$.update; 36 | component.current.$$.update = function() { 37 | watchers.forEach(([name, callback]) => { 38 | const index = component.current.$$.props[name]; 39 | callback(component.current.$$.ctx[index]); 40 | }); 41 | update.apply(null, arguments); 42 | }; 43 | } 44 | 45 | return () => { 46 | component.current.$destroy(); 47 | }; 48 | }, []); 49 | 50 | useEffect(() => { 51 | if (!mounted) { 52 | setMount(true); 53 | return; 54 | } 55 | 56 | component.current.$set(props); 57 | }, [props]); 58 | 59 | return React.createElement(tag, { ref: container, style }); 60 | }; 61 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // import pkg from "./package.json"; 2 | 3 | // export default { 4 | // input: "./src/adaptors.js", 5 | // external: ["react", "vue"], 6 | // output: [ 7 | // { 8 | // file: pkg.main, 9 | // format: "cjs" 10 | // }, 11 | // { 12 | // file: pkg.module, 13 | // format: "esm" 14 | // } 15 | // ] 16 | // }; 17 | -------------------------------------------------------------------------------- /src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pngwn/svelte-adapter/a733a1781daf36f8a68aebe0377cf116390e31dd/src/assets/favicon.png -------------------------------------------------------------------------------- /src/assets/features.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Instant Prototyping", 4 | "text": "Quickly scaffold new projects with your preferred view library and toolkit. Kick it off with a perfect Lighthouse score!" 5 | }, 6 | { 7 | "title": "Feature Rich", 8 | "text": "Supports Babel, Bublé, Browserlist, TypeScript, PostCSS, ESLint, Prettier, and Service Workers out of the box!" 9 | }, 10 | { 11 | "title": "Fully Extensible", 12 | "text": "Includes a plugin system that allows for easy, fine-grain control of your configuration... when needed." 13 | }, 14 | { 15 | "title": "Plug 'n Play", 16 | "text": "Don't worry about configuration, unless you want to. Presets and plugins are automatically applied. Just install and go!" 17 | }, 18 | { 19 | "title": "Framework Agnostic", 20 | "text": "Build with your preferred framework or with none at all! Official presets for Preact, React, Vue, and Svelte." 21 | }, 22 | { 23 | "title": "Static Site Generator", 24 | "text": "Export your routes as \"pre-rendered\" HTML, which is great for SEO and works on any static hosting service." 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pngwn/svelte-adapter/a733a1781daf36f8a68aebe0377cf116390e31dd/src/assets/icon.png -------------------------------------------------------------------------------- /src/assets/link.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PWA/CLI", 3 | "short_name": "PWA/CLI", 4 | "background_color": "#3E82F7", 5 | "orientation": "portrait", 6 | "theme_color": "#1e88e5", 7 | "display": "standalone", 8 | "start_url": "/", 9 | "icons": [{ 10 | "src": "/assets/icon.png", 11 | "type": "image/png", 12 | "sizes": "512x512" 13 | }] 14 | } 15 | -------------------------------------------------------------------------------- /src/assets/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /src/assets/shapes/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/shapes/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/shapes/penta.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/shapes/point.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/shapes/square.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/twitter_heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pngwn/svelte-adapter/a733a1781daf36f8a68aebe0377cf116390e31dd/src/assets/twitter_heart.png -------------------------------------------------------------------------------- /src/components/App.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 |
20 |

I am a Svelte component

21 |

{number}

22 | 25 |
26 | -------------------------------------------------------------------------------- /src/components/App/index.css: -------------------------------------------------------------------------------- 1 | .app { 2 | position: relative; 3 | } 4 | 5 | .wrapper { 6 | width: 75%; 7 | margin: 0 auto; 8 | position: relative; 9 | min-height: calc(55vh - 16px); 10 | z-index: 1; 11 | } 12 | 13 | .section { 14 | padding-bottom: 64px; 15 | } 16 | 17 | .section h2 { 18 | margin-bottom: 16px; 19 | } 20 | 21 | @media screen and (max-width: 769px) { 22 | .wrapper { 23 | width: 90%; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/App/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import SvelteCropper from "svelte-easy-crop"; 5 | import toReact from "../../../react"; 6 | 7 | // import "./style.css"; 8 | 9 | const Cropper = toReact( 10 | SvelteCropper, 11 | { width: "500px", height: "500px", position: "relative" }, 12 | "div" 13 | ); 14 | let image = 15 | "https://images.unsplash.com/photo-1518020382113-a7e8fc38eac9?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2776&q=80"; 16 | let crop = { x: 0, y: 0 }; 17 | 18 | const buttons = { 19 | position: "absolute", 20 | left: 0, 21 | right: 0, 22 | bottom: "10px" 23 | }; 24 | 25 | export default function() { 26 | const [showCropper, toggleShow] = useState(false); 27 | const [data, setData] = useState({}); 28 | const [zoom, setZoom] = useState(1); 29 | 30 | return ( 31 |
32 | 33 |
34 |
{JSON.stringify(data, null, 2)}
35 |
36 | {showCropper ? ( 37 | <> 38 | setData(e.detail)} 40 | watchZoom={z => setZoom(z)} 41 | image={image} 42 | zoom={zoom} 43 | /> 44 |
45 | 48 |
49 | 50 | ) : null} 51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/components/App/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/components/App/style.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | 6 | button { 7 | background: cadetblue; 8 | font-size: 16px; 9 | padding: 5px 10px; 10 | color: #fff; 11 | border: none; 12 | border-radius: 2px; 13 | margin: 0px 10px; 14 | } 15 | 16 | .data { 17 | text-align: left; 18 | position: absolute; 19 | background: #eee; 20 | width: 300px; 21 | height: 300px; 22 | z-index: 2; 23 | } 24 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pngwn/svelte-adapter/a733a1781daf36f8a68aebe0377cf116390e31dd/src/index.css -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PWA 5 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | 23 | 29 | 30 | 31 | 32 | 33 |
34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "@components/App"; 4 | 5 | import Vue from "vue"; 6 | import App2 from "@components/App/index.vue"; 7 | 8 | import "./index.css"; 9 | 10 | ReactDOM.render(, document.getElementById("app")); 11 | 12 | new Vue({ 13 | el: "#app2", 14 | template: "", 15 | components: { App2 } 16 | }); 17 | -------------------------------------------------------------------------------- /vue.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | 3 | export default (Component, style = {}, tag = "span") => 4 | Vue.component("vue-svelte-adaptor", { 5 | render(createElement) { 6 | return createElement(tag, { 7 | ref: "container", 8 | props: this.$attrs, 9 | style 10 | }); 11 | }, 12 | data() { 13 | return { 14 | comp: null 15 | }; 16 | }, 17 | mounted() { 18 | this.comp = new Component({ 19 | target: this.$refs.container, 20 | props: this.$attrs 21 | }); 22 | 23 | let watchers = []; 24 | 25 | for (const key in this.$listeners) { 26 | this.comp.$on(key, this.$listeners[key]); 27 | const watchRe = /watch:([^]+)/; 28 | 29 | const watchMatch = key.match(watchRe); 30 | 31 | if (watchMatch && typeof this.$listeners[key] === "function") { 32 | watchers.push([ 33 | `${watchMatch[1][0].toLowerCase()}${watchMatch[1].slice(1)}`, 34 | this.$listeners[key] 35 | ]); 36 | } 37 | } 38 | 39 | if (watchers.length) { 40 | let comp = this.comp; 41 | const update = this.comp.$$.update; 42 | this.comp.$$.update = function() { 43 | watchers.forEach(([name, callback]) => { 44 | const index = comp.$$.props[name]; 45 | callback(comp.$$.ctx[index]); 46 | }); 47 | update.apply(null, arguments); 48 | }; 49 | } 50 | }, 51 | updated() { 52 | this.comp.$set(this.$attrs); 53 | }, 54 | destroyed() { 55 | this.comp.$destroy(); 56 | } 57 | }); 58 | --------------------------------------------------------------------------------