├── .eslintignore ├── .eslintrc.cjs ├── .github └── workflows │ ├── coverage.yml │ └── publish.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── app.d.ts ├── app.html ├── lib │ ├── ComponentRenderer.svelte │ ├── PropsRenderer.svelte │ ├── Render.svelte │ ├── createRender.ts │ ├── index.ts │ └── store.ts ├── routes │ └── +page.svelte └── tests │ ├── button.svelte │ ├── interactive-rocket.svelte │ ├── multiple-rockets.svelte │ ├── render-children.test.ts │ ├── render-components.test.ts │ ├── render-primitives.test.ts │ ├── rocket.svelte │ └── template.svelte ├── svelte.config.js ├── tsconfig.json ├── vite.config.ts └── vitest.setup.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type { import("eslint").Linter.FlatConfig } */ 2 | module.exports = { 3 | root: true, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:svelte/recommended', 8 | 'prettier' 9 | ], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['@typescript-eslint'], 12 | parserOptions: { 13 | sourceType: 'module', 14 | ecmaVersion: 2020, 15 | extraFileExtensions: ['.svelte'] 16 | }, 17 | env: { 18 | browser: true, 19 | es2017: true, 20 | node: true 21 | }, 22 | overrides: [ 23 | { 24 | files: ['*.svelte'], 25 | parser: 'svelte-eslint-parser', 26 | parserOptions: { 27 | parser: '@typescript-eslint/parser' 28 | } 29 | } 30 | ] 31 | }; 32 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: coveralls test coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | release: 9 | types: 10 | - created 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: 18 20 | - run: npm i svelte 21 | - run: npm ci 22 | - run: npm test 23 | - uses: coverallsapp/github-action@master 24 | with: 25 | github-token: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: publish 5 | 6 | on: 7 | release: 8 | types: [published] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 18 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: 18 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm run package 32 | - run: npm publish . 33 | env: 34 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /dist 5 | /.svelte-kit 6 | /package 7 | .env 8 | .env.* 9 | !.env.example 10 | vite.config.js.timestamp-* 11 | vite.config.ts.timestamp-* 12 | /coverage 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore files for PNPM, NPM and YARN 2 | pnpm-lock.yaml 3 | package-lock.json 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 100, 6 | "bracketSpacing": false, 7 | "plugins": ["prettier-plugin-svelte"], 8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bryan Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # svelte-render 2 | 3 | [![npm version](http://img.shields.io/npm/v/svelte-render.svg)](https://www.npmjs.com/package/svelte-render) 4 | [![npm downloads](https://img.shields.io/npm/dm/svelte-render.svg)](https://www.npmjs.com/package/svelte-render) 5 | ![license](https://img.shields.io/npm/l/svelte-render) 6 | ![build](https://img.shields.io/github/actions/workflow/status/bryanmylee/svelte-render/publish.yml) 7 | [![coverage](https://coveralls.io/repos/github/bryanmylee/svelte-render/badge.svg?branch=main)](https://coveralls.io/github/bryanmylee/svelte-render?branch=main) 8 | [![size](https://img.shields.io/bundlephobia/min/svelte-render)](https://bundlephobia.com/result?p=svelte-render) 9 | 10 | Manage complex Svelte behaviors outside of templates with full type safety. 11 | 12 | ```svelte 13 | 21 | 22 | 23 | ``` 24 | 25 | ## Installation 26 | 27 | ```bash 28 | $ npm i -D svelte-render 29 | ``` 30 | 31 | ## API 32 | 33 | Svelte Render was primarily built to support complex rendering definitions for [Svelte Headless Table](https://github.com/bryanmylee/svelte-headless-table). You can find full documentation on `createRender` on the [documentation site](https://svelte-headless-table.bryanmylee.com/docs/api/create-render). 34 | 35 | ### `` 36 | 37 | `` handles props and automatically registers the event handlers defined with `.on` as well as slot data defined with `.slot`. 38 | 39 | `of` accepts: 40 | 41 | - primitive data such as `number` and `string` 42 | - `Writable` and `Writable` for dynamic primitive data 43 | - `ComponentRenderConfig` returned by `createRender` 44 | 45 | ```svelte 46 | 49 | 50 | 51 | ``` 52 | 53 | becomes 54 | 55 | ```svelte 56 | 57 | ``` 58 | 59 | ### `createRender: (component, props)` 60 | 61 | `createRender` accepts a Svelte component and its props as arguments. 62 | 63 | `props` can be omitted if the component does not receive props but must be included otherwise. 64 | 65 | ```ts 66 | const icon = createRender(TickIcon); // ✅ 67 | const avatar = createRender(Avatar); // ❌ Type error. 68 | const avatar = createRender(Avatar, {name: 'Ada Lovelace'}); // ✅ 69 | ``` 70 | 71 | If you need prop reactivity, `props` must be a [Svelte store](https://svelte.dev/tutorial/writable-stores). 72 | 73 | ```ts 74 | const avatarProps = writable({name: 'Ada Lovelace'}); 75 | const avatar = createRender(Avatar, avatarProps); 76 | ``` 77 | 78 | ### `.on(event, handler)` 79 | 80 | Svelte Render supports the Svelte event system by chaining `.on` calls on `createRender()`. Multiple event handlers can be registered for the same event type like the Svelte `on:` directive. 81 | 82 | ```ts 83 | const button = createRender(Button) 84 | .on('click', handleClick) 85 | .on('click', (ev) => console.log(ev)); 86 | ``` 87 | 88 | `` becomes: 89 | 90 | ```svelte 91 | 123 | ``` 124 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-render", 3 | "description": "Manage complex Svelte behaviors outside of templates with full type safety", 4 | "version": "2.0.1", 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build && npm run package", 8 | "preview": "vite preview", 9 | "package": "svelte-kit sync && svelte-package && publint", 10 | "prepublishOnly": "npm run package", 11 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 12 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 13 | "test": "vitest run --coverage", 14 | "test:only": "vitest run", 15 | "test:watch": "vitest", 16 | "lint": "prettier --check . && eslint .", 17 | "format": "prettier --write ." 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+ssh://git@github.com/bryanmylee/svelte-render.git" 22 | }, 23 | "keywords": [ 24 | "svelte", 25 | "sveltejs", 26 | "render", 27 | "template" 28 | ], 29 | "author": "Bryan Lee", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/bryanmylee/svelte-render/issues" 33 | }, 34 | "homepage": "https://github.com/bryanmylee/svelte-render#readme", 35 | "exports": { 36 | ".": { 37 | "types": "./dist/index.d.ts", 38 | "svelte": "./dist/index.js", 39 | "default": "./dist/index.js" 40 | } 41 | }, 42 | "files": [ 43 | "dist", 44 | "!dist/**/*.test.*", 45 | "!dist/**/*.spec.*" 46 | ], 47 | "peerDependencies": { 48 | "svelte": "^4.0.0" 49 | }, 50 | "devDependencies": { 51 | "@sveltejs/adapter-auto": "^3.0.0", 52 | "@sveltejs/kit": "^2.0.0", 53 | "@sveltejs/package": "^2.0.0", 54 | "@sveltejs/vite-plugin-svelte": "^3.0.0", 55 | "@testing-library/jest-dom": "^6.1.6", 56 | "@testing-library/svelte": "^4.0.5", 57 | "@testing-library/user-event": "^14.5.2", 58 | "@typescript-eslint/eslint-plugin": "^6.0.0", 59 | "@typescript-eslint/parser": "^6.0.0", 60 | "@vitest/coverage-v8": "^1.1.1", 61 | "eslint": "^8.28.0", 62 | "eslint-config-prettier": "^9.1.0", 63 | "eslint-plugin-svelte": "^2.30.0", 64 | "jsdom": "^23.0.1", 65 | "prettier": "^3.1.1", 66 | "prettier-plugin-svelte": "^3.1.2", 67 | "publint": "^0.1.9", 68 | "svelte": "^4.2.7", 69 | "svelte-check": "^3.6.0", 70 | "tslib": "^2.4.1", 71 | "typescript": "^5.0.0", 72 | "vite": "^5.0.3", 73 | "vitest": "^1.0.0" 74 | }, 75 | "dependencies": { 76 | "svelte-subscribe": "^2.0.0" 77 | }, 78 | "svelte": "./dist/index.js", 79 | "types": "./dist/index.d.ts", 80 | "type": "module" 81 | } 82 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/ComponentRenderer.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 | {#if isReadable(config.props)} 30 | 31 | 32 | 33 | {:else} 34 | 35 | {/if} 36 | -------------------------------------------------------------------------------- /src/lib/PropsRenderer.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | {#if config.children.length === 0} 14 | 15 | {:else} 16 | 17 | {#each config.children as child} 18 | 19 | {/each} 20 | 21 | {/if} 22 | -------------------------------------------------------------------------------- /src/lib/Render.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | {#if isReadable(config)} 15 | 16 | {$readableConfig} 17 | {:else if typeof config !== 'object'} 18 | {config} 19 | {:else} 20 | 21 | {/if} 22 | -------------------------------------------------------------------------------- /src/lib/createRender.ts: -------------------------------------------------------------------------------- 1 | import type {ComponentEvents, ComponentProps, SvelteComponent} from 'svelte'; 2 | import type {Readable} from 'svelte/store'; 3 | 4 | export type RenderConfig = 5 | | ComponentRenderConfig 6 | | string 7 | | number 8 | | Readable; 9 | 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | export type Constructor = new (...args: any[]) => TInstance; 12 | 13 | export class ComponentRenderConfig { 14 | constructor( 15 | public component: Constructor, 16 | public props?: ComponentProps | Readable>, 17 | ) {} 18 | 19 | eventHandlers: [keyof ComponentEvents, (ev: Event) => void][] = []; 20 | on>( 21 | type: TEventType, 22 | handler: (ev: ComponentEvents[TEventType]) => void, 23 | ): this { 24 | this.eventHandlers.push([type, handler]); 25 | return this; 26 | } 27 | 28 | children: RenderConfig[] = []; 29 | slot(...children: RenderConfig[]) { 30 | this.children = children; 31 | return this; 32 | } 33 | } 34 | 35 | // Allow omission of the `props` argument if the component accepts no props. 36 | export function createRender>>( 37 | component: Constructor, 38 | ): ComponentRenderConfig; 39 | 40 | export function createRender( 41 | component: Constructor, 42 | props: ComponentProps | Readable>, 43 | ): ComponentRenderConfig; 44 | 45 | export function createRender( 46 | component: Constructor, 47 | props?: ComponentProps | Readable>, 48 | ): ComponentRenderConfig { 49 | return new ComponentRenderConfig(component, props); 50 | } 51 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export {default as Render} from './Render.svelte'; 2 | export * from './createRender.js'; 3 | -------------------------------------------------------------------------------- /src/lib/store.ts: -------------------------------------------------------------------------------- 1 | import {readable, type Readable} from 'svelte/store'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | export const isReadable = (value: any): value is Readable => { 5 | return value?.subscribe instanceof Function; 6 | }; 7 | 8 | export const Undefined = readable(undefined); 9 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |

Welcome to your library project

16 |

Create your package using @sveltejs/package and preview/showcase your work with SvelteKit

17 |

Visit kit.svelte.dev to read the documentation

18 | 19 |
20 | 21 |
22 | 23 |
24 | 25 |
26 | 27 |
28 | 29 |
30 | -------------------------------------------------------------------------------- /src/tests/button.svelte: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/tests/interactive-rocket.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/tests/multiple-rockets.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | {#each {length: times} as _} 6 | 🚀 7 | {/each} 8 | -------------------------------------------------------------------------------- /src/tests/render-children.test.ts: -------------------------------------------------------------------------------- 1 | import {createRender} from '$lib/index.js'; 2 | import {render, screen} from '@testing-library/svelte'; 3 | import Button from './button.svelte'; 4 | import Rocket from './rocket.svelte'; 5 | import Template from './template.svelte'; 6 | 7 | it('renders a component with children', () => { 8 | const config = createRender(Button).slot(createRender(Rocket), 'Fire!'); 9 | render(Template, {props: {config}}); 10 | expect(screen.getByTestId('button')).toHaveTextContent('🚀Fire!'); 11 | }); 12 | -------------------------------------------------------------------------------- /src/tests/render-components.test.ts: -------------------------------------------------------------------------------- 1 | import {createRender} from '$lib/index.js'; 2 | import {act, render, screen} from '@testing-library/svelte'; 3 | import {writable} from 'svelte/store'; 4 | import Rocket from './rocket.svelte'; 5 | import MultipleRockets from './multiple-rockets.svelte'; 6 | import Template from './template.svelte'; 7 | 8 | it('renders a component with no props', () => { 9 | const config = createRender(Rocket); 10 | render(Template, {props: {config}}); 11 | expect(screen.getByTestId('template')).toHaveTextContent('🚀'); 12 | }); 13 | 14 | it('renders a component with static props', () => { 15 | const config = createRender(MultipleRockets, {times: 3}); 16 | render(Template, {props: {config}}); 17 | expect(screen.getByTestId('template')).toHaveTextContent('🚀🚀🚀'); 18 | }); 19 | 20 | it('renders a component with reactive props', async () => { 21 | const props = writable({times: 3}); 22 | const config = createRender(MultipleRockets, props); 23 | render(Template, {props: {config}}); 24 | expect(screen.getByTestId('template')).toHaveTextContent('🚀🚀🚀'); 25 | props.set({times: 4}); 26 | await act(); 27 | expect(screen.getByTestId('template')).toHaveTextContent('🚀🚀🚀🚀'); 28 | }); 29 | -------------------------------------------------------------------------------- /src/tests/render-primitives.test.ts: -------------------------------------------------------------------------------- 1 | import {act, render, screen} from '@testing-library/svelte'; 2 | import {writable} from 'svelte/store'; 3 | import Template from './template.svelte'; 4 | 5 | it('renders a static string', () => { 6 | render(Template, {props: {config: 'Ada Lovelace'}}); 7 | expect(screen.getByTestId('template')).toHaveTextContent('Ada Lovelace'); 8 | }); 9 | 10 | it('renders a static number', () => { 11 | render(Template, {props: {config: 1337}}); 12 | expect(screen.getByTestId('template')).toHaveTextContent('1337'); 13 | }); 14 | 15 | it('renders a reactive string', async () => { 16 | const config = writable('Ada Lovelace'); 17 | render(Template, {props: {config}}); 18 | expect(screen.getByTestId('template')).toHaveTextContent('Ada Lovelace'); 19 | config.set('Alan Turing'); 20 | await act(); 21 | expect(screen.getByTestId('template')).toHaveTextContent('Alan Turing'); 22 | }); 23 | 24 | it('renders a reactive number', async () => { 25 | const config = writable(1337); 26 | render(Template, {props: {config}}); 27 | expect(screen.getByTestId('template')).toHaveTextContent('1337'); 28 | config.set(42); 29 | await act(); 30 | expect(screen.getByTestId('template')).toHaveTextContent('42'); 31 | }); 32 | -------------------------------------------------------------------------------- /src/tests/rocket.svelte: -------------------------------------------------------------------------------- 1 | 🚀 2 | -------------------------------------------------------------------------------- /src/tests/template.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 11 |
12 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 12 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 14 | adapter: adapter() 15 | } 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "module": "NodeNext", 13 | "moduleResolution": "NodeNext", 14 | "types": ["vitest/globals"], 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import {sveltekit} from '@sveltejs/kit/vite'; 2 | import {defineConfig} from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | test: { 7 | globals: true, 8 | environment: 'jsdom', 9 | setupFiles: ['vitest.setup.ts'], 10 | coverage: {reporter: 'lcov'}, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest'; 2 | --------------------------------------------------------------------------------