{"_id":"@7willows/sw-lib","name":"@7willows/sw-lib","dist-tags":{"latest":"3.0.5"},"versions":{"3.0.5":{"name":"@7willows/sw-lib","description":"Base toolset for building web components with preact","version":"3.0.5","license":"ISC","main":"dist/build.js","types":"index.d.ts","author":{"name":"7willows"},"bugs":{"url":"https://github.com/7willows/sw-lib/issues"},"homepage":"https://github.com/7willows/sw-lib#readme","scripts":{"build:defs":"npm-dts generate","build":"esbuild src/index.ts --bundle --outfile=dist/build.js --sourcemap --loader:.css=text --format=esm","build:dev":"esbuild src/test-client/index.jsx --bundle --outfile=src/test-client/build.js --sourcemap --jsx-factory=h --jsx-fragment=Fragment --inject:./preact-shim.js --loader:.css=text","build:prod":"npm run build -- --minify --sourcemap --target=chrome96,firefox91,safari15,edge96","build:watch":"run-p esbuild:watch tsc:watch","build:watch:dev":"npm run build:dev -- --watch","tsc:watch":"tsc --watch","esbuild:watch":"npm run build -- --watch","test-client":"live-server . --open=/src/test-client/index.html --watch=dist/,src/**/*.html, src/**/*.css","dev":"concurrently --kill-others \"live-server test-client\" \"npm run build:watch:dev\"","lint":"eslint src/"},"devDependencies":{"esbuild":"^0.15.7","eslint":"^7.32.0","live-server":"^1.2.1","lodash":"^4.17.21","npm-dts":"^1.3.12","npm-run-all":"^4.1.5","to-no-case":"^1.0.2","tslib":"^2.4.1","typescript":"^4.5.4"},"dependencies":{"@types/lodash":"^4.14.177","@types/moment":"^2.13.0","@types/preact-custom-element":"^4.0.1","lodash-es":"^4.17.21","preact":"^10.5.14","preact-custom-element":"^4.2.1","ts-pattern":"^3.3.4"},"gitHead":"5dc04f71d517c54bd68aa22ebff3f45710339158","_id":"@7willows/sw-lib@3.0.5","_nodeVersion":"16.18.1","_npmVersion":"8.19.2","dist":{"integrity":"sha512-kHscox/nOGpMtyqBpwKtFoH+/j/mJ0vSLXq0Owk7dJBwSZCGbrSfk426X8c9loZt+taJ7A9Hn+Pc1+0cuig4NA==","shasum":"6b5291a7cc0db9200672e3e678f5830e9ecdf610","tarball":"https://registry.npmjs.org/@7willows/sw-lib/-/sw-lib-3.0.5.tgz","fileCount":77,"unpackedSize":3961428,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIEs+Sl2kc2bGzgI2o26JTUATJOsal3dZzu1ibtmHedQrAiEAutNVhintAKWS+cyz+dxzmEk/SUh+0Gzdhjqwl40Dcjg="}],"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJkUeNcACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2VmowdQ//UzNY16ehykCmjkA8mG0lBTGHByaGrV4C3nkDoiW/Jgzk76nH\r\nNQ8gSLlf4F/w3zCToAEQ08eKNdLEm8cRf2PV+Z4fd0XT1qYsRMpjNtDDz8vJ\r\nMEf3DF+E4HXmGXABJdoA+JLXzch5jQVIF8xfdQKDLPV0DzAJ5bn8ikHasm4s\r\nQ3vxe635Z3euK5CeJKlfNa3SQEGbEot15bEYZSed7+euniaTxTkoMkx5qE3e\r\n2ZHrckGftt4LlutQdr+wigOSNaIou3vquMV0ke0uAPKVGurXMsquVeZVqb/x\r\nlrRgyPm2NMREw/GZqGwbsTt4rsGkl7Cm0fXMjlNVQVUo/qTm43QIEKz/LCBR\r\nEtuc2c23aNwlRkHzkHGob6YOTgtGqyesqvcK9SaZRh+MBvHdKHODh070Ni9g\r\n2XlYHYxuH+/P8GgEyJZOE3+5rGzp0URBvo16e3xne/E/t50FoG2S9m1536W3\r\n5m0g/6czgBGlkpS2OjLNA2BPnryt+8EAah9cj4LN8hMDdGOacTouqXpdP/pe\r\nhUzlN5sEUXv57iR4aS6iKEcL/JcZAuwo6S0kXBE+umD5eZtd3fmkmoCu3TaS\r\nwxZAFhfw5Mcbilk280RSXufh3zkIgTxAfvqRWz+FnW5TnFhKtEruMSgBVJ/p\r\nwiXlXZSDOzpT23722meyfr4+QBflw4GF2N0=\r\n=wQUY\r\n-----END PGP SIGNATURE-----\r\n"},"_npmUser":{"name":"7willows","email":"sw@7willows.com"},"directories":{},"maintainers":[{"name":"7willows","email":"sw@7willows.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/sw-lib_3.0.5_1683088220393_0.4869016662433423"},"_hasShrinkwrap":false}},"time":{"created":"2023-05-03T04:30:20.337Z","3.0.5":"2023-05-03T04:30:20.736Z","modified":"2023-05-03T04:30:20.968Z"},"maintainers":[{"name":"7willows","email":"sw@7willows.com"}],"description":"Base toolset for building web components with preact","homepage":"https://github.com/7willows/sw-lib#readme","author":{"name":"7willows"},"bugs":{"url":"https://github.com/7willows/sw-lib/issues"},"license":"ISC","readme":"# sw-lib\n\n## Table of Contents\n\n- web-components\n    - [sw-button](#sw-button)\n    - [sw-loader](#sw-loader)\n    - [sw-number-input](#sw-number-input)\n    - [sw-pagination](#sw-pagination)\n    - [sw-table](#sw-table)\n    - [sw-text-input](#sw-text-input)\n- utils\n    - [sw-flash-message](#sw-flash-message)\n    - [sw-modal](#sw-modal)\n\t- [stm - Preact State Manager and Web Component builder](#stm)\n\n<a name=\"sw-button\"></a>\n\n## sw-button\n\n### HTML\n\nTo use a component provide the tag `sw-button` with required attributes:\n\n- `icon` -\n- `disabled` -\n\nExample of use:\n\n```html\n<sw-button\n        icon=\"magnifier\"\n        disabled=\"false\">\n</sw-button>\n```\n\n### CSS\n\nYou can style the component changing the following options in the `:host` selector:\n\n- `--foreground-color`\n- `--primary-color-70`\n- `--primary-color-40`\n- `--primary-color-100`\n\n\n- Example of use:\n\n```css\n:host {\n    --foreground-color: white;\n    --primary-color-70: rgba(98, 191, 124, .7);\n    --primary-color-40: rgba(98, 191, 124, .4);\n    --primary-color-100: rgba(98, 191, 124, 1);\n}\n```\n\n<a name=\"sw-modal\"></a>\n\n## sw-modal\n\n### JS\n\nTo use the sw-modal call the `modal()` function, which takes an object as an argument. The object should contain the following keys:\n\n- `header` - can be a `string` or a `function({ close }):string` or a `function({ close }):JSX` or `JSX`\n- `body` - can be a `string` or a `function({ close }):string` or a `function({ close }):JSX` or `JSX`\n- `footer` - can be a `string` or a `function({ close }):string` or a `function({ close }):JSX` or `JSX`\n- `large` - `boolean`\n\nThe object cannot be specified without any parameters.\n\nExample of use:\n\n```jsx\nconst result = await modal({\n    header: 'Welcome',\n    body: <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>,\n    footer: ({ close }) => <sw-button onClick={() => close(true)}>OK</sw-button>,\n    large: true\n})\n```\n\n### CSS\n\nYou can style the component changing the following options in the `:host` selector on your index.html:\n\n- `--background-color` - modal background color\n- `--radius` - modal border radius\n\nExample of use:\n\n```css\n:host {\n    --background-color: ghostwhite;\n    --radius: 5px;\n}\n```\n\n<a name=\"stm\"></a>\n## STM - State Manager\n\n`stm` is a state manager for preact. It's main concept is borrowed from ELM Architecture, although it differs in many edge cases. The goal of he `stm` is to create a **web component** which could be plugged into any other framework. \n\nThis tutorial will present various examples that will help you understand how to work with `stm`. The examples can be found in the `examples` directory of this repository.\n\n### \"Hello World\"\n\n*index.html*\n```html\n<!doctype html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"UTF-8\"/>\n        <title>STM Hello World example</title>\n        <script src=\"./index.compiled.js\"></script>\n    </head>\n    <body>\n        <hello-world></hello-world>\n    </body>\n</html>\n```\n\n*index.tsx (compiled to **index.compiled.js**)*\n```ts\nimport { stm } from '@7willows/sw-lib';\n\nstm.component({\n    tagName: 'hello-world',\n    init() {\n        return [{}, null];\n    },\n    update() {\n        return [{}, null];\n    },\n    view\n});\n\nfunction view() {\n    return <p>Hello World!</p>\n}\n```\n\nthe compilation was done using this command:\n\n```bash\nesbuild index.tsx --bundle --outfile=index.compiled.js --sourcemap --jsx-factory=h --jsx-fragment=Fragment --inject:./preact-shim.js --format=esm\n```\n\n`stm.component` accepts a configuration object. In the above example we are specifying the minimum set of arguments needed for the component to run.\n\nMake notice the `tagName` property. It must be a tag name of the new Web Component. The specification of custom elements requires the tag name to have a dash (`-`) that's why we cannot simply name our component: `hello` - this won't work. The good practice is to have a prefix for a certain library or project and use this prefix everywhere. For example in this library the prefix for all web components is `sw-`.\n\n### Basic interactivity\n\nIn the previous example we created a component that does nothig. To add some interactivity we need to understand the data flow of a stm component:\n\n![STM data flow](./stm-diagram.svg \"STM data flow\")\n\nThere are two most importand concepts in `stm`:\n\n1. **state** - a state is any data that represents current state of a component.\n2. **msg** - a message represents a change. It may be user clicking a button, form input or some outside change like a received data from a websocket connection.\n\nLet's write a **hello-who** component which will demonstrate the above mentioned data flow:\n\n*index.html*\n```\n<!doctype html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"UTF-8\"/>\n        <title>STM Hello Who example</title>\n        <script src=\"./index.compiled.js\"></script>\n    </head>\n    <body>\n        <hello-who></hello-who>\n    </body>\n</html>\n```\n\n*index.tsx (compiled to **index.compiled.js**)*\n```ts\nimport { stm } from '@7willows/sw-lib';\n\ntype State = {\n    display: 'idle' | 'question' | 'welcoming';\n    name: string;\n}\ntype Msg\n    = { type: 'introduce' }\n    | { type: 'input', name: string }\n    | { type: 'confirmName' }\n\nstm.component({\n    tagName: 'hello-who',\n    init(): [State, stm.Cmd<Msg>] {\n        return [\n            { display: 'idle', name: '' },\n            null\n        ];\n    },\n    update(state: State, msg: Msg) {\n        if (msg.type === 'introduce') {\n            return [\n                { ...state, display: 'question' },\n                null\n            ];\n        }\n\n        if (msg.type === 'input') {\n            return [\n                { ...state, name: msg.name },\n                null\n            ]\n        }\n\n        if (msg.type === 'confirmName') {\n            return [\n                { ...state, display: 'welcoming' },\n                null\n            ]\n        }\n        return [state, null];\n    },\n    view\n});\n\nfunction view(state: State) {\n    if (state.display === 'idle') {\n        return <button onClick={{ type: 'introduce' }}>Introduce yourself</button>;\n    }\n    if (state.display === 'question') {\n        return <div>\n            <input type=\"text\" onInput={(event: any) => ({ type: 'input', name: event.target.value })} />\n            <button onClick={{ type: 'confirmName' }}>OK</button>\n        </div>\n    }\n    return <p>Hello {state.name}!</p>;\n}\n```\n\nMake notice that both `init` and `update` functions return an array of two elements. The first element is a new state and the second is a command. Commands are required if we want to do something outside the component but we will talk about them later. In current component we don't need any commands so we just use `null` as a command.\n\nImportant things to consider in the above example:\n\n1. `init` returns a new state (along with a null in place of command)\n2. next `view` renders html according to the `state` returned by the `init` function\n3. when user interacts with the view `msg` is returned (see: `<button onClick={{ type: 'introduce' }}>Introduce yourself</button>` i.e.). The event handler can also be a function like in here: `<input type=\"text\" onInput={(event: any) => ({ type: 'input', name: event.target.value })} />` - this form can be used when we need an `event` object).\n4. the generated `msg` goes to the `update(state, msg)` function\n5. the `update` function returns a new `state` with a command (in our case the command is `null`).\n6. the `view` function is executed again with the new `state` returned from `update` and the rerender is invoked.\n7. and so on in a loop...\n\n### Debug mode\n\nEvery view is rendered based on state, this means that having a state we can recreate how the app looked like in any moment. Quite often when debugging a `stm` component you will want to know what was the state before certain msg and what was the state after a msg. To show this info enable `debug` in the `stm.component` arguments:\n\n```ts\nstm.component({\n    tagName: 'hello-who',\n\tdebug: true,\n\tinit, \n\tupdate,\n    view\n});\n```\n\nNow every change to state will be logged in a devtools console.\n\n### Shadow DOM\n\nto enable shadow dom use `shadow: true` in the component options:\n\n```ts\nstm.component({\n    tagName: 'hello-who',\n\tshadow: true,\n\tinit, \n\tupdate,\n    view\n});\n```\n\nat this point the web component will be rendered in a shadow dom.\n\n### Component attributes\n\nIt's often the case that a web component has some attributes. Moreover the attributes are not something static - they can change at any given moment. To deal with changing attributes you have to translate an attribute to a msg, because this is how we deal with changes in `stm`. Additionally we need to specify the list of expected attributes with their types. Let's have a simple example: a counter that will increase it's displayed value every second - however this time the displayed value will come from the outside.\n\n*index.html*\n```html\n<!doctype html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"UTF-8\"/>\n        <title>STM counter example</title>\n        <script src=\"./index.compiled.js\"></script>\n    </head>\n    <body>\n        <my-counter count=\"0\"></my-counter>\n\n        <script>\n         const $counter = document.querySelector('my-counter');\n         let count = 0;\n         \n         setInterval(function () {\n             count += 1;\n             $counter.setAttribute('count', count);\n         }, 1000);\n        </script>\n    </body>\n</html>\n```\n\nThe above script every second changes the `count` attribute of `my-counter` component.\n\n\n*index.tsx (compiled to **index.compiled.js**)*\n```ts\nimport { stm } from '@7willows/sw-lib';\n\ntype State = {\n    count: number;\n}\ntype Msg = { type: 'attr', name: string, value: unknown }\n\nstm.component({\n    tagName: 'my-counter',\n    propTypes: {\n        count: Number\n    },\n    attributeChangeFactory: (name, value) => ({ type: 'attr' as const, name, value }),\n    init(): [State, stm.Cmd<Msg>] {\n        return [\n            { count: 0 },\n            null\n        ];\n    },\n    update,\n    view\n});\n\nfunction update(state: State, msg: Msg): [State, stm.Cmd<Msg>] {\n    if (msg.type === 'attr' && typeof msg.value === 'string') {\n        state.count = parseInt(msg.value, 10);\n    } else if (msg.type === 'attr' && typeof msg.value === 'number') {\n        state.count = msg.value;\n    }\n\n    return [state, null];\n}\n\nfunction view(state: State) {\n    return <p>Current count: {state.count}</p>\n}\n```\n\nMake notice:\n\n1. we have to specify the list of attributes in: `propTypes`\n2. the `propTypes` take attribute name and attribute type. The type of an attribute is a a value constructor (Boolean, Number, String, Object, Array)\n3. you have to translate the attribute change into a msg. This is done in `attributeChangeFactory`\n4. the message: `{ type: 'attr', name: string, value: unknown }` has the `value` property of type `unknown` because we don't know what the outside world will pass to our component. That's why we need to be prepared for an unknown. Normally all attributes are strings however if this web component would be put into some other preact/react application then the specific data type would be passed instead of a string. That's why we should cover both cases:\n\n```ts\nif (msg.type === 'attr' && typeof msg.value === 'string') {\n    state.count = parseInt(msg.value, 10);\n} else if (msg.type === 'attr' && typeof msg.value === 'number') {\n    state.count = msg.value;\n}\n```\n\n### Making an asynchronous call \n\nTo make an async call we have to take advantage of the `stm.Cmd<Msg>` property returned from both `init` and `update` functions. This is known as a \"command\". It might be a `Promise<Msg>`, `stm.Focus()` or a custom event that should be dispatched on this component. To make an async call we will set a promise for `Msg` as a command. Consider this example:\n\n\n*index.html*\n```html\n<!doctype html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"UTF-8\"/>\n        <title>STM GitHub Users example</title>\n        <script src=\"./index.compiled.js\"></script>\n    </head>\n    <body>\n        <gh-users query=\"7willows\"></gh-users>\n    </body>\n</html>\n```\n\n*index.tsx (compiled to **index.compiled.js**)*\n```ts\nimport { stm } from '@7willows/sw-lib';\n\ninterface User {\n    login: string;\n}\n\ntype State = {\n    isLoading: boolean;\n    hasLoadError: boolean;\n    query: string;\n    users: User[];\n}\n\ntype Msg\n    = { type: 'loadFailed' }\n    | { type: 'loadSuccess', users: User[] }\n    | { type: 'attr', name: string, value: unknown }\n\nstm.component({\n    tagName: 'gh-users',\n    debug: true,\n    attributeChangeFactory: (name, value) => ({ type: 'attr', name, value }),\n    init(): [State, stm.Cmd<Msg>] {\n        return [\n            {\n                isLoading: false,\n                hasLoadError: false,\n                users: [],\n                query: '',\n            },\n            null\n        ];\n    },\n    update,\n    view\n});\n\nfunction update(state: State, msg: Msg): [State, stm.Cmd<Msg>] {\n    if (msg.type === 'attr' && typeof msg.value === 'string') {\n        state.query = msg.value;\n        state.isLoading = true;\n        state.hasLoadError = false;\n        return [state, loadUsers(msg.value)]\n    }\n    if (msg.type === 'loadFailed') {\n        state.hasLoadError = true;\n        state.isLoading = false;\n        return [state, null];\n    }\n    if (msg.type === 'loadSuccess') {\n        state.hasLoadError = false;\n        state.isLoading = false;\n        state.users = msg.users;\n        return [state, null];\n    }\n    return [state, null];\n}\n\nasync function loadUsers(query: string): Promise<Msg> {\n    try {\n        const res = await fetch(`https://api.github.com/search/users?q=${query}`)\n        const json = await res.json();\n        return { type: 'loadSuccess', users: json.items };\n    } catch (err) {\n        console.error(err);\n        return { type: 'loadFailed' }\n    }\n}\n\nfunction view(state: State) {\n    return <div class=\"gh-users\">\n        <h2>Github Users search for phrase: {state.query}</h2>\n        {state.isLoading && <div class=\"loader\">loading...</div>}\n        {state.hasLoadError && <div class=\"danger\">Error occured</div>}\n        found:\n        <ul>\n            <>\n                {state.users.map(user => <li>\n                    {user.login}\n                </li>)}\n            </>\n        </ul>\n    </div>\n}\n```\n\nThe flow of the program:\n\n1. when \"query\" attribute is changed (or set initially), the `update` function returns a state with a command. In our case a command is a promise for msg: `return [state, loadUsers(msg.value)]`.\n2. in the `loadUsers` we make an async call and return a `Msg`.\n3. as a result the returned msg is an input for the next invocation of the `update` function.\n4. at this point `update` function changes its state by setting the users\n5. next `view` renders the users\n\nMake notice that when we started an async request the state was changed (`isLoading` property was added to the state). It is a good practice to show user that something is loading. Not every user has a good internet connection.\n\n\n### Dealing with outside world changes\n\nSometimes we have to listen to some events that are not DOM based. In such cases we have to manually dispatch a msg. A dispatched msg is used as an argument for the `update` function.\n\n*index.html*\n```html\n<!doctype html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"UTF-8\"/>\n        <title>STM Resizer example</title>\n        <script src=\"./index.compiled.js\"></script>\n    </head>\n    <body>\n        <my-resizer></my-resizer>\n    </body>\n</html>\n```\n\n*index.tsx (compiled to **index.compiled.js**)*\n```ts\nimport { stm } from '@7willows/sw-lib';\n\ntype State = {\n    width: number;\n    height: number;\n}\n\ntype Msg = { type: 'resize', width: number, height: number }\n\nstm.component({\n    tagName: 'my-resizer',\n    willMount(cmp: any, dispatch: stm.Dispatch<Msg>) {\n        cmp.onResize = function() {\n            dispatch({\n                type: 'resize',\n                width: window.innerWidth,\n                height: window.innerHeight\n            });\n        }\n        window.addEventListener('resize', cmp.onResize);\n    },\n    willUnmount(cmp: any) {\n        window.removeEventListener('resize', cmp.onResize);\n    },\n    init(): [State, stm.Cmd<Msg>] {\n        return [\n            { width: 100, height: 100 },\n            null\n        ];\n    },\n    update,\n    view\n});\n\nfunction update(state: State, msg: Msg): [State, stm.Cmd<Msg>] {\n    if (msg.type === 'resize') {\n        state.width = msg.width / 2;\n        state.height = msg.height / 2;\n        return [state, null];\n    }\n    return [state, null];\n}\n\nfunction view(state: State) {\n    return <div style={getStyles(state)}></div>\n}\n\nfunction getStyles(state: State) {\n    return `\nbackground-color: red;\nwidth: ${state.width}px;\nheight: ${state.height}px;\n    `\n}\n```\n\nThis component resizes its div whenever a window resize happens. Of course it would be much simpler to use `width: 50%; height: 50%` to do the job, but this is just an example.\n\nImportant thigs to notice in this example:\n\n1. `willMount` is a function that is called whenever the component is about to be inserted into the DOM\n2. `willUnmount` is a function that is called just before an element is removed from DOM\n3. the `cmp` attribute of `willMount` and `willUnmount` is a preact component object. You cannot do much with it (and you shouldn't) however it serves very wall as an holder for storing handlers that should be later removed\n4. whenever you add some event listener in `willMount` don't forget to remove it in `willUnmount`\n5. The `init` function also receives the `dispatch` function as an argument however it's usually better to use `willMount` and `willUnmount` functions for dealing with outside changes because `willUnmount` allows us to clean up.\n","readmeFilename":"README.md"}