├── .eslintrc.js ├── .nojekyll ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── babel.config.json ├── build │ ├── dist │ │ ├── advanced-usage │ │ │ ├── manually-observe-state │ │ │ │ ├── index.js │ │ │ │ ├── observe-all-state-vars │ │ │ │ │ ├── automatic-component.js │ │ │ │ │ ├── manual-component.js │ │ │ │ │ └── state.js │ │ │ │ └── observe-specific-state-vars │ │ │ │ │ ├── specific-automatic-component.js │ │ │ │ │ ├── specific-manual-component.js │ │ │ │ │ └── state.js │ │ │ ├── state-recorder-usage.js │ │ │ └── state-var-handler │ │ │ │ ├── custom-state-var-handler-component.js │ │ │ │ ├── index.js │ │ │ │ └── state.js │ │ ├── api │ │ │ ├── lit-state.js │ │ │ ├── observe-state-mixin.js │ │ │ ├── state-recorder.js │ │ │ ├── state-var-decorator.js │ │ │ └── state-var-handler.js │ │ ├── basic-usage │ │ │ ├── index.js │ │ │ ├── no-decorator-usage │ │ │ │ ├── index.js │ │ │ │ ├── no-deco-component-1.js │ │ │ │ ├── no-deco-component-2.js │ │ │ │ └── state.js │ │ │ ├── state-var-component-1.js │ │ │ ├── state-var-component-2.js │ │ │ └── state.js │ │ ├── demo-component.js │ │ ├── index.js │ │ ├── intro-page.js │ │ ├── lit-state.js │ │ ├── state-handling │ │ │ ├── computed-values │ │ │ │ ├── computed-value-component.js │ │ │ │ ├── index.js │ │ │ │ └── state.js │ │ │ ├── index.js │ │ │ └── nested-states │ │ │ │ ├── index.js │ │ │ │ ├── nested-state-component.js │ │ │ │ └── states.js │ │ └── use-cases │ │ │ ├── different-vars-on-rerender │ │ │ ├── changing-component.js │ │ │ ├── control-component.js │ │ │ ├── index.js │ │ │ └── state.js │ │ │ ├── index.js │ │ │ └── reconnected-components │ │ │ ├── index.js │ │ │ ├── reconnect-control-component.js │ │ │ ├── reconnecting-component.js │ │ │ └── state.js │ ├── favicon.png │ ├── index.html │ ├── snowpack │ │ └── env.js │ └── web_modules │ │ ├── common │ │ ├── lit-element-336867a2.js │ │ └── lit-style-ca615be5.js │ │ ├── import-map.json │ │ ├── lit-docs.js │ │ ├── lit-element-style.js │ │ └── lit-element.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.png │ └── index.html ├── snowpack.config.js └── src │ ├── advanced-usage │ ├── manually-observe-state │ │ ├── index.js │ │ ├── observe-all-state-vars │ │ │ ├── automatic-component.js │ │ │ ├── manual-component.js │ │ │ └── state.js │ │ └── observe-specific-state-vars │ │ │ ├── specific-automatic-component.js │ │ │ ├── specific-manual-component.js │ │ │ └── state.js │ ├── state-recorder-usage.js │ └── state-var-handler │ │ ├── custom-state-var-handler-component.js │ │ ├── index.js │ │ └── state.js │ ├── api │ ├── lit-state.js │ ├── observe-state-mixin.js │ ├── state-recorder.js │ ├── state-var-decorator.js │ └── state-var-handler.js │ ├── basic-usage │ ├── index.js │ ├── no-decorator-usage │ │ ├── index.js │ │ ├── no-deco-component-1.js │ │ ├── no-deco-component-2.js │ │ └── state.js │ ├── state-var-component-1.js │ ├── state-var-component-2.js │ └── state.js │ ├── demo-component.js │ ├── index.js │ ├── intro-page.js │ ├── lit-state.js │ ├── state-handling │ ├── computed-values │ │ ├── computed-value-component.js │ │ ├── index.js │ │ └── state.js │ ├── index.js │ └── nested-states │ │ ├── index.js │ │ ├── nested-state-component.js │ │ └── states.js │ └── use-cases │ ├── different-vars-on-rerender │ ├── changing-component.js │ ├── control-component.js │ ├── index.js │ └── state.js │ ├── index.js │ └── reconnected-components │ ├── index.js │ ├── reconnect-control-component.js │ ├── reconnecting-component.js │ └── state.js ├── lit-state.js ├── package-lock.json └── package.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser': true, 4 | 'es6': true 5 | }, 6 | 'extends': 'eslint:recommended', 7 | 'globals': { 8 | 'Atomics': 'readonly', 9 | 'SharedArrayBuffer': 'readonly' 10 | }, 11 | "parser": "babel-eslint", 12 | 'parserOptions': { 13 | 'ecmaVersion': 2018, 14 | 'sourceType': 'module' 15 | }, 16 | 'rules': { 17 | 'indent': [ 18 | 'error', 19 | 4, 20 | { 21 | 'SwitchCase': 1 22 | } 23 | ], 24 | 'linebreak-style': [ 25 | 'error', 26 | 'unix' 27 | ], 28 | 'semi': [ 29 | 'error', 30 | 'always' 31 | ] 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitaarik/lit-state/8cd66223612c3b115c0275f58f6cee5e900ee534/.nojekyll -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # LitState Changelog 2 | 3 | 4 | ### 1.7.0 - Non-decorator usage simplified 5 | 6 | - The static `stateVars()` getter method should now return an object with in 7 | the keys the name of the stateVar and in the values the initial value of the 8 | stateVar. 9 | - The stateVar options were previously set with the static `stateVars()` getter 10 | method, but should now be specified with the static `stateVarOptions()` 11 | getter method. 12 | - Docs updated accordingly. 13 | - Added some more descriptive comments in the code. 14 | 15 | #### To upgrade from 1.6.x 16 | 17 | For non-decorator usage, update the static `stateVar()` getter methods. 18 | 19 | Change this: 20 | 21 | ```javascript 22 | class MyState extends LitState { 23 | 24 | static get stateVars() { 25 | return { 26 | myStateVar: { option: 'value' } 27 | }; 28 | } 29 | 30 | constructor() { 31 | super(); 32 | this.myStateVar = 'initial value'; 33 | } 34 | 35 | } 36 | ``` 37 | 38 | To this: 39 | 40 | ```javascript 41 | class MyState extends LitState { 42 | 43 | static get stateVars() { 44 | return { 45 | myStateVar: 'initial value' 46 | }; 47 | } 48 | 49 | static get stateVarOptions() { 50 | return { 51 | myStateVar: { option: 'value' } 52 | }; 53 | } 54 | 55 | } 56 | ``` 57 | 58 | If the options object is empty, you don't need to specify the static 59 | `stateVarOptions()` getter method. 60 | 61 | 62 | ### 1.6.1 - Support reconnecting components 63 | 64 | - When a component gets dynamically removed from the DOM and then added again, 65 | call `this.requestUpdate()` so that the observers get-readded again. Fixes 66 | [issue #5](https://github.com/gitaarik/lit-state/issues/5). 67 | - Added a docs page for this use case. 68 | - Reordered docs menu. 69 | - Added some comments in the code. 70 | 71 | 72 | ### 1.6.0 - Added JavaScript decorator support 73 | 74 | - The way `stateVar` variables are defined has changed. You have to update this 75 | coming from 1.5.0. 76 | - Now you can use decorators to specify your stateVars, use `@stateVar() myVar 77 | = 'initial value';` in your state class. 78 | - You can also still define `stateVar` variables without decorators. It is done 79 | in a similar way LitElement does. Check the docs for this. 80 | - Updated LitDocs and documentation content. 81 | - Overall refactors. 82 | 83 | #### To upgrade from 1.5.x 84 | 85 | Update the `stateVar` definitions, instead of: 86 | 87 | ```javascript 88 | myData = stateVar('value'); 89 | ``` 90 | 91 | Do this: 92 | 93 | ```javascript 94 | @stateVar() myData = 'value'; 95 | ``` 96 | 97 | For no-decorator usage, [look in the docs](https://gitaarik.github.io/lit-state/build/#basic-usage/no-decorator-usage/). 98 | 99 | 100 | ### 1.5.0 - Moved `asyncStateVar` to separate repository 101 | 102 | - The `asyncStateVar` is now available [here](https://github.com/gitaarik/lit-state-async-state-var) 103 | so that LitState is tiny for projects that don't use `asyncStateVar`. 104 | - Updated docs and demo app. 105 | 106 | 107 | ### 1.4.0 - Mixin primary means of making components state-aware 108 | 109 | - Making mixin usage the primary means for making your components state-aware. 110 | - Renamed the mixin class `LitStateElementMixin` to `observeState`, reads 111 | nicer, looks better, is shorter. 112 | - Updated docs and demo app to use the new mixin class. 113 | 114 | 115 | ### 1.3.0 - LitState independent from StateVar classes 116 | 117 | - `LitState` is now not dependend on the `StateVar` and `AsyncStateVar` 118 | classes. There's one `BaseStateVar` class which all types of stateVar classes 119 | should extend from. `LitState` recognizes this as a stateVar, and then 120 | handles the handling of the `get` and `set` traps to the stateVar. This 121 | allows for the possibility to factor out `AsyncStateVar` to a separate 122 | library, making LitState smaller. Because not all apps always need 123 | `AsyncStateVar`. It also allows for easily adding more (custom) types of 124 | stateVars, to extend LitState. 125 | - Some more general refactors to make things simpler. 126 | - Reordered some texts in README 127 | 128 | 129 | ### 1.2.3 130 | 131 | - Fixed bug where `isRejectedGet()` still returned `true` when a `setValue()` 132 | failed after it, and `isRejectedSet()` still returned `true` when a 133 | `getValue()` failed after it. 134 | 135 | 136 | ### 1.2.2 137 | 138 | - Updated Readme 139 | 140 | 141 | ### 1.2.1 142 | 143 | - Updated Readme 144 | 145 | 146 | ### 1.2.0 147 | 148 | - Assigning to an `asyncStateVar` now throws an error, you have to use 149 | `setValue()`. This to make usage consistent. 150 | - `setValue()` doesn't return a promise anymore. 151 | - `_pendingCache` is always set to `false` when `setValue()` succeeds, so 152 | `pushCache()` doesn't need `setValue()` to return a promise anymore. 153 | - Removed unnecessary `logStateVar()` calls for `asyncStateVar`. Only 154 | components that call `getValue()` need to re-render when the var changes. 155 | - Added demo page for components that show different state variables on 156 | re-renders. 157 | - Overall demo app refactors and UI/text improvements. 158 | 159 | 160 | ### 1.1.2 161 | 162 | - Added `setCache(value)` and `pushCache()` functionality to `asyncStateVar`. 163 | - Added demo page for `setCache(value)` and `pushCache()`. 164 | - Updated readme. 165 | 166 | 167 | ### 1.1.1 168 | 169 | - Created mixin `LitStateElementMixin`. 170 | - Added demo page for mixin `LitStateElementMixin`. 171 | - Updated demo app internals. 172 | 173 | 174 | ### 1.1.0 175 | 176 | - Created changelog. 177 | - Changed behavior of `asyncStateVar()`: 178 | - You can now have a **getter** and **setter**. 179 | - `getValue()` won't return the error from the promise's reject callback in 180 | case of an error anymore. You can get the error with the method 181 | `getError()` (for the error of the last get or set), `getErrorGet()` (for 182 | the error of the last get) or `getErrorSet()` (for the error of the last 183 | set). 184 | - Updated demo app to showcase `asyncStateVar` better. 185 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LitState 2 | 3 | ### Simple shared app state management for LitElement. 4 | 5 | LitState automatically re-renders your LitElement components, when a shared app 6 | state variable they use changes. It's like LitElement's 7 | [properties](https://lit-element.polymer-project.org/guide/properties), but 8 | then shared over multiple components. 9 | 10 | It's tiny, simple but still powerful, just like 11 | [LitElement](https://lit-element.polymer-project.org/) and 12 | [lit-html](https://lit-html.polymer-project.org/). 13 | 14 | 15 | ## Installation 16 | 17 | ``` 18 | npm install lit-element-state 19 | ``` 20 | 21 | *The name `lit-state` is unfortunately already taken on npm, so therefore the 22 | slightly awkward package name.* 23 | 24 | 25 | ### Current version: v1.7.0 26 | 27 | For more information and update instructions, [See the changelog](CHANGELOG.md). 28 | 29 | 30 | ## Basic idea 31 | 32 | You keep your shared state in a `LitState` derived class. This class contains 33 | `stateVar` variables that contain the state. This class can also contain helper 34 | functions that modify the state. Decorate your `LitElement` classes with the 35 | `observeState()` mixin. This makes your components automatically re-render 36 | whenever a `stateVar` they use changes. 37 | 38 | 39 | ## Usage 40 | 41 | ### 1. Create a `LitState` object: 42 | 43 | ```javascript 44 | import { LitState, stateVar } from 'lit-element-state'; 45 | 46 | class MyState extends LitState { 47 | @stateVar() counter = 0; 48 | } 49 | 50 | export const myState = new MyState(); 51 | ``` 52 | 53 | #### Or without [javascript decorators](https://github.com/tc39/proposal-decorators): 54 | 55 | ```javascript 56 | import { LitState, stateVar } from 'lit-element-state'; 57 | 58 | class MyState extends LitState { 59 | static get stateVars() { 60 | return { 61 | counter: 0 62 | }; 63 | } 64 | } 65 | 66 | export const myState = new MyState(); 67 | ``` 68 | 69 | ### 2. Make your component aware of your state: 70 | 71 | By using the `observeState()` mixin on your `LitElement` class and then just 72 | using the `stateVar` variables in your render method: 73 | 74 | ```javascript 75 | import { LitElement, html } from 'lit-element'; 76 | import { observeState } from 'lit-element-state'; 77 | import { myState } from './my-state.js'; 78 | 79 | class MyComponent extends observeState(LitElement) { 80 | 81 | render() { 82 | return html` 83 |

Counter: ${myState.counter}

84 | 85 | `; 86 | } 87 | 88 | } 89 | ``` 90 | 91 | The components that read `myState.counter` will automatically re-render when 92 | any (other) component updates it. 93 | 94 | In more technical words: 95 | 96 | A component using the `observeState()` mixin will re-render when any 97 | `stateVar` - which it read in the last render cycle - changes. 98 | 99 | 100 | ## Docs 101 | 102 | For more information about how to use LitState, check the 103 | [docs](https://gitaarik.github.io/lit-state/build/). 104 | 105 | 106 | ## How does this work? 107 | 108 | 109 | ### Basics 110 | 111 | When you define a `stateVar` variable, LitState will observe those variables 112 | whenever they're get or set. When using the `observeState()` mixin on a 113 | component, during the render of that component, there is a recorder active that 114 | records any `stateVar` that is accessed during the render of that component. At 115 | the end of the render, the recorded `stateVar` variables are collected and 116 | whenever one of them changes, the component will be re-rendered. If the 117 | re-render uses different `stateVar` variables, they are again recorded and 118 | observed for possible rerenders. 119 | 120 | 121 | ### Implementation details 122 | 123 | To re-render the component, the `observeState()` mixin calls LitElement's 124 | [`this.requestUpdate()`](https://lit-element.polymer-project.org/api/classes/_lit_element_.litelement.html#requestupdate) 125 | (with no arguments). This will enqueue an update request for the component. The 126 | component will re-render at the end of the execution queue. 127 | `this.requestUpdate()` can be called multiple times during a particular 128 | JavaScript event (like a click), and it will only update the component once, at 129 | the end of the execution queue. So it doesn't matter when it is called multiple 130 | times when multiple `stateVar` variables are changed during a JavaScript event. 131 | This is an optimization feature built-in in LitElement. LitElement uses this 132 | optimization for it's own 133 | [properties](https://lit-element.polymer-project.org/guide/properties). This 134 | optimization works in the same way for LitState's `stateVar` variables. 135 | 136 | Also, LitElement uses lit-html, which sees which parts of the template are 137 | changed or not. And it will only re-render the HTML elements that have changes. 138 | 139 | 140 | ## Notes 141 | 142 | ### You can create and use multiple `LitState` classes at the same time. 143 | 144 | It is even encouraged to keep things separate. You can of course have one big 145 | `LitState` derived class which contains all global app state variables. But it 146 | is probably cleaner if you categorize it into multiple smaller `LitState` 147 | derived classes. For example, you can put each state class in a separate file, 148 | collected in a `state/` folder, and import them at the places you need. 149 | 150 | 151 | ### You can nest states 152 | 153 | If your state requires more hierarchy, you can also nest states. It doesn't 154 | matter to your components how your state is structured, as long as it uses the 155 | correct references to your `stateVar` variables. Refer to the 156 | [docs](https://gitaarik.github.io/lit-state/build/#state-handling/nested-states/) 157 | for more information about nesting states. 158 | 159 | 160 | ### Only new assigns trigger a re-render. Updating an object/array won't trigger a re-render. 161 | 162 | Just like LitElement's 163 | [properties](https://lit-element.polymer-project.org/guide/properties), only a 164 | new assign of the `stateVar` triggers a re-render. For example if you have a 165 | state like this: 166 | 167 | ```javascript 168 | MyState extends LitState { 169 | @stateVar() myObj = {myKey: 'myValue'}; 170 | @stateVar() myArray = ['one', 'two', 'three']; 171 | } 172 | ``` 173 | 174 | Then this won't trigger a re-render: 175 | 176 | ```javascript 177 | myState = new MyState(); 178 | myState.myObj.mykey = 'newValue'; 179 | myState.myArray.push('four'); 180 | ``` 181 | 182 | You'll instead need to assign a new object to the `stateVar`: 183 | 184 | ```javascript 185 | myState.myObj = {...myState.myObj, myKey: 'newValue'}; 186 | myState.myArray = [...myState.myArray, 'four']; 187 | ``` 188 | 189 | Watching for changes inside objects is very complex matter and would make 190 | LitState way more complicated than desirable. If you are interested in this 191 | kind of thing, check out 192 | [observable-slim](https://github.com/ElliotNB/observable-slim). 193 | 194 | 195 | ## Extra features 196 | 197 | ### Custom `stateVar` variables 198 | 199 | You can easily extend LitState with a custom `stateVar` handler. An example of 200 | this is the [asyncStateVar](https://github.com/gitaarik/lit-state-async-state-var), 201 | which is a `stateVar` variation that makes handling with asynchronous data 202 | easy. To make a custom `stateVar` yourself, create a class that extends from 203 | `StateVar`, exported by LitState. 204 | [Check out the documentation on this.](https://gitaarik.github.io/lit-state/build/#advanced-usage/state-var-handler/) 205 | 206 | 207 | ## FAQ 208 | 209 | 210 | ### Why should I use shared state for my components? Doesn't that oppose the concept of web components? 211 | 212 | The big feature of web components is that they are encapsulated through the 213 | [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM). 214 | That means that their internal state isn't affected by state from the outside. 215 | And also that the component's internal state doesn't affect other elements on 216 | the page. This makes web components great for creating reusable elements. 217 | Reusable elements should have no side-effects, meaning that they shouldn't 218 | change state outside of themselves. 219 | 220 | Reusable elements are great and we should use them a lot. When you're building 221 | a full application however, it is also desirable to have application-specific 222 | components that have application-specific side-effects. For example, changing 223 | the global app state. And it is of course desirable, that when this global app 224 | state changes, the components that use this global app state are synchronized 225 | with it. 226 | 227 | And you can also have a reusable component that has several internal 228 | sub-components. They all might need to share some common internal state. 229 | 230 | LitState is created for these use cases, and is meant to make it as simple as 231 | possible for the developer. 232 | 233 | ### Why not use Redux or MobX? Any benefits by using this? 234 | 235 | To use [Redux](https://github.com/reduxjs/redux) (or something similar, like 236 | [unistore](https://github.com/developit/unistore)) you need a lot of of boiler 237 | plate code: *Actions*, *Action Creators* and *Reducers*, you need to dispatch 238 | created actions and you need to map the state to your props inside your 239 | components. You have helper libraries that make it a bit easier, but I still 240 | think it is unnecessarily complicated. 241 | 242 | I think [MobX](https://github.com/mobxjs/mobx) is much easier to use, because 243 | you don't need to write any boilerplate. However, MobX is a quite large library 244 | with a lot of whistles and bells. And for more advanced use-cases it can become 245 | relatively complicated to use. 246 | 247 | I think a lot of features from MobX are not really necessary when you use 248 | LitElement. MobX is mainly created for React. Therefore MobX has optimizations 249 | aimed at how React works. LitState is specifically aimed at LitElement. And 250 | most of the optimizations MobX created for React are not required for 251 | LitElement. 252 | 253 | See the section [How does this work?](#how-does-this-work) to see how LitState 254 | works together with LitElement. 255 | 256 | Also LitState doesn't try to track changes inside objects, like MobX does. That 257 | is also a reason why MobX is complicated. It's nice that you can modify objects 258 | and MobX detects that, but it's not very hard to just set a new object. That 259 | makes the source code of LitState a lot smaller and simpler, and therefore also 260 | easier to understand what is happening. 261 | [Look here](#only-new-assigns-trigger-a-re-render-updating-a-objectarray-wont-trigger-a-re-render) 262 | for more details on this. 263 | 264 | Basically it comes down to the fact that LitState is written for, and with the 265 | same philosophy as, LitElement and lit-html. Which makes it more suitable for 266 | developers that like this philosophy. 267 | 268 | If you however do want to use MobX with LitElement for some reason, check out 269 | [lit-mobx](https://github.com/adobe/lit-mobx). 270 | 271 | 272 | ## Development 273 | 274 | LitState is brand-new. I created it because I wanted an easy way to deal with 275 | shared app state in LitElement, for my own projects. I hope it can make the 276 | lives of other developers easier too. 277 | 278 | I want to keep LitState small and simple, just like LitElement. So I don't 279 | expect to add a lot of features. Only things that are a very common patterns 280 | for shared app state management would be suitable to include. 281 | 282 | In any case, I will keep expanding the documentation to make the library more 283 | accessible. Also I would like to add unit tests, to automatically test the 284 | library. I don't have much experience with unit testing in JavaScript, so I 285 | need to dive into that. 286 | 287 | If you have comments, suggestions, questions, any kind of feedback, or you want 288 | to contribute, I would be pleased to hear from you. Feel free to open an issue. 289 | 290 | 291 | ## Also see 292 | 293 | - [asyncStateVar](https://github.com/gitaarik/lit-state-async-state-var) - asyncStateVar for LitState, easy handing of async data 294 | - [LitStyle](https://github.com/gitaarik/lit-style) - Shared component styles for LitElement 295 | - [LitDocumentEvent](https://github.com/gitaarik/lit-document-event) - Easily add listeners to the document object 296 | - [LitDocs](https://github.com/gitaarik/lit-docs) - Utilities to create documentation for LitElement related projects 297 | -------------------------------------------------------------------------------- /docs/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser': true, 4 | 'es6': true 5 | }, 6 | 'extends': 'eslint:recommended', 7 | 'globals': { 8 | 'Atomics': 'readonly', 9 | 'SharedArrayBuffer': 'readonly' 10 | }, 11 | "parser": "babel-eslint", 12 | 'parserOptions': { 13 | 'ecmaVersion': 2018, 14 | 'sourceType': 'module', 15 | 'ecmaFeatures': { 16 | 'legacyDecorators': true 17 | } 18 | }, 19 | 'rules': { 20 | 'indent': [ 21 | 'error', 22 | 4, 23 | { 24 | 'SwitchCase': 1 25 | } 26 | ], 27 | 'linebreak-style': [ 28 | 'error', 29 | 'unix' 30 | ], 31 | 'semi': [ 32 | 'error', 33 | 'always' 34 | ] 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | node_modules 3 | -------------------------------------------------------------------------------- /docs/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # New Project 2 | 3 | > ✨ Bootstrapped with Create Snowpack App (CSA). 4 | 5 | ## Available Scripts 6 | 7 | ### npm start 8 | 9 | Runs the app in the development mode. 10 | Open http://localhost:8080 to view it in the browser. 11 | 12 | The page will reload if you make edits. 13 | You will also see any lint errors in the console. 14 | 15 | ### npm run build 16 | 17 | Builds the app for production to the `dist/` folder. 18 | It correctly bundles React in production mode and optimizes the build for the best performance. 19 | 20 | ## Directives 21 | 22 | In case you need to add a directive like `classMap` you should add the extension to the import: 23 | 24 | ``` 25 | import { classMap } from "lit-html/directives/class-map.js"; 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": true }], 4 | ["@babel/plugin-proposal-class-properties", { "loose": true }] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /docs/build/dist/advanced-usage/manually-observe-state/observe-all-state-vars/state.js: -------------------------------------------------------------------------------- 1 | function _decorate(decorators, factory, superClass, mixins) { var api = _getDecoratorsApi(); if (mixins) { for (var i = 0; i < mixins.length; i++) { api = mixins[i](api); } } var r = factory(function initialize(O) { api.initializeInstanceElements(O, decorated.elements); }, superClass); var decorated = api.decorateClass(_coalesceClassElements(r.d.map(_createElementDescriptor)), decorators); api.initializeClassElements(r.F, decorated.elements); return api.runClassFinishers(r.F, decorated.finishers); } 2 | 3 | function _getDecoratorsApi() { _getDecoratorsApi = function () { return api; }; var api = { elementsDefinitionOrder: [["method"], ["field"]], initializeInstanceElements: function (O, elements) { ["method", "field"].forEach(function (kind) { elements.forEach(function (element) { if (element.kind === kind && element.placement === "own") { this.defineClassElement(O, element); } }, this); }, this); }, initializeClassElements: function (F, elements) { var proto = F.prototype; ["method", "field"].forEach(function (kind) { elements.forEach(function (element) { var placement = element.placement; if (element.kind === kind && (placement === "static" || placement === "prototype")) { var receiver = placement === "static" ? F : proto; this.defineClassElement(receiver, element); } }, this); }, this); }, defineClassElement: function (receiver, element) { var descriptor = element.descriptor; if (element.kind === "field") { var initializer = element.initializer; descriptor = { enumerable: descriptor.enumerable, writable: descriptor.writable, configurable: descriptor.configurable, value: initializer === void 0 ? void 0 : initializer.call(receiver) }; } Object.defineProperty(receiver, element.key, descriptor); }, decorateClass: function (elements, decorators) { var newElements = []; var finishers = []; var placements = { static: [], prototype: [], own: [] }; elements.forEach(function (element) { this.addElementPlacement(element, placements); }, this); elements.forEach(function (element) { if (!_hasDecorators(element)) return newElements.push(element); var elementFinishersExtras = this.decorateElement(element, placements); newElements.push(elementFinishersExtras.element); newElements.push.apply(newElements, elementFinishersExtras.extras); finishers.push.apply(finishers, elementFinishersExtras.finishers); }, this); if (!decorators) { return { elements: newElements, finishers: finishers }; } var result = this.decorateConstructor(newElements, decorators); finishers.push.apply(finishers, result.finishers); result.finishers = finishers; return result; }, addElementPlacement: function (element, placements, silent) { var keys = placements[element.placement]; if (!silent && keys.indexOf(element.key) !== -1) { throw new TypeError("Duplicated element (" + element.key + ")"); } keys.push(element.key); }, decorateElement: function (element, placements) { var extras = []; var finishers = []; for (var decorators = element.decorators, i = decorators.length - 1; i >= 0; i--) { var keys = placements[element.placement]; keys.splice(keys.indexOf(element.key), 1); var elementObject = this.fromElementDescriptor(element); var elementFinisherExtras = this.toElementFinisherExtras((0, decorators[i])(elementObject) || elementObject); element = elementFinisherExtras.element; this.addElementPlacement(element, placements); if (elementFinisherExtras.finisher) { finishers.push(elementFinisherExtras.finisher); } var newExtras = elementFinisherExtras.extras; if (newExtras) { for (var j = 0; j < newExtras.length; j++) { this.addElementPlacement(newExtras[j], placements); } extras.push.apply(extras, newExtras); } } return { element: element, finishers: finishers, extras: extras }; }, decorateConstructor: function (elements, decorators) { var finishers = []; for (var i = decorators.length - 1; i >= 0; i--) { var obj = this.fromClassDescriptor(elements); var elementsAndFinisher = this.toClassDescriptor((0, decorators[i])(obj) || obj); if (elementsAndFinisher.finisher !== undefined) { finishers.push(elementsAndFinisher.finisher); } if (elementsAndFinisher.elements !== undefined) { elements = elementsAndFinisher.elements; for (var j = 0; j < elements.length - 1; j++) { for (var k = j + 1; k < elements.length; k++) { if (elements[j].key === elements[k].key && elements[j].placement === elements[k].placement) { throw new TypeError("Duplicated element (" + elements[j].key + ")"); } } } } } return { elements: elements, finishers: finishers }; }, fromElementDescriptor: function (element) { var obj = { kind: element.kind, key: element.key, placement: element.placement, descriptor: element.descriptor }; var desc = { value: "Descriptor", configurable: true }; Object.defineProperty(obj, Symbol.toStringTag, desc); if (element.kind === "field") obj.initializer = element.initializer; return obj; }, toElementDescriptors: function (elementObjects) { if (elementObjects === undefined) return; return _toArray(elementObjects).map(function (elementObject) { var element = this.toElementDescriptor(elementObject); this.disallowProperty(elementObject, "finisher", "An element descriptor"); this.disallowProperty(elementObject, "extras", "An element descriptor"); return element; }, this); }, toElementDescriptor: function (elementObject) { var kind = String(elementObject.kind); if (kind !== "method" && kind !== "field") { throw new TypeError('An element descriptor\'s .kind property must be either "method" or' + ' "field", but a decorator created an element descriptor with' + ' .kind "' + kind + '"'); } var key = _toPropertyKey(elementObject.key); var placement = String(elementObject.placement); if (placement !== "static" && placement !== "prototype" && placement !== "own") { throw new TypeError('An element descriptor\'s .placement property must be one of "static",' + ' "prototype" or "own", but a decorator created an element descriptor' + ' with .placement "' + placement + '"'); } var descriptor = elementObject.descriptor; this.disallowProperty(elementObject, "elements", "An element descriptor"); var element = { kind: kind, key: key, placement: placement, descriptor: Object.assign({}, descriptor) }; if (kind !== "field") { this.disallowProperty(elementObject, "initializer", "A method descriptor"); } else { this.disallowProperty(descriptor, "get", "The property descriptor of a field descriptor"); this.disallowProperty(descriptor, "set", "The property descriptor of a field descriptor"); this.disallowProperty(descriptor, "value", "The property descriptor of a field descriptor"); element.initializer = elementObject.initializer; } return element; }, toElementFinisherExtras: function (elementObject) { var element = this.toElementDescriptor(elementObject); var finisher = _optionalCallableProperty(elementObject, "finisher"); var extras = this.toElementDescriptors(elementObject.extras); return { element: element, finisher: finisher, extras: extras }; }, fromClassDescriptor: function (elements) { var obj = { kind: "class", elements: elements.map(this.fromElementDescriptor, this) }; var desc = { value: "Descriptor", configurable: true }; Object.defineProperty(obj, Symbol.toStringTag, desc); return obj; }, toClassDescriptor: function (obj) { var kind = String(obj.kind); if (kind !== "class") { throw new TypeError('A class descriptor\'s .kind property must be "class", but a decorator' + ' created a class descriptor with .kind "' + kind + '"'); } this.disallowProperty(obj, "key", "A class descriptor"); this.disallowProperty(obj, "placement", "A class descriptor"); this.disallowProperty(obj, "descriptor", "A class descriptor"); this.disallowProperty(obj, "initializer", "A class descriptor"); this.disallowProperty(obj, "extras", "A class descriptor"); var finisher = _optionalCallableProperty(obj, "finisher"); var elements = this.toElementDescriptors(obj.elements); return { elements: elements, finisher: finisher }; }, runClassFinishers: function (constructor, finishers) { for (var i = 0; i < finishers.length; i++) { var newConstructor = (0, finishers[i])(constructor); if (newConstructor !== undefined) { if (typeof newConstructor !== "function") { throw new TypeError("Finishers must return a constructor."); } constructor = newConstructor; } } return constructor; }, disallowProperty: function (obj, name, objectType) { if (obj[name] !== undefined) { throw new TypeError(objectType + " can't have a ." + name + " property."); } } }; return api; } 4 | 5 | function _createElementDescriptor(def) { var key = _toPropertyKey(def.key); var descriptor; if (def.kind === "method") { descriptor = { value: def.value, writable: true, configurable: true, enumerable: false }; } else if (def.kind === "get") { descriptor = { get: def.value, configurable: true, enumerable: false }; } else if (def.kind === "set") { descriptor = { set: def.value, configurable: true, enumerable: false }; } else if (def.kind === "field") { descriptor = { configurable: true, writable: true, enumerable: true }; } var element = { kind: def.kind === "field" ? "field" : "method", key: key, placement: def.static ? "static" : def.kind === "field" ? "own" : "prototype", descriptor: descriptor }; if (def.decorators) element.decorators = def.decorators; if (def.kind === "field") element.initializer = def.value; return element; } 6 | 7 | function _coalesceGetterSetter(element, other) { if (element.descriptor.get !== undefined) { other.descriptor.get = element.descriptor.get; } else { other.descriptor.set = element.descriptor.set; } } 8 | 9 | function _coalesceClassElements(elements) { var newElements = []; var isSameElement = function (other) { return other.kind === "method" && other.key === element.key && other.placement === element.placement; }; for (var i = 0; i < elements.length; i++) { var element = elements[i]; var other; if (element.kind === "method" && (other = newElements.find(isSameElement))) { if (_isDataDescriptor(element.descriptor) || _isDataDescriptor(other.descriptor)) { if (_hasDecorators(element) || _hasDecorators(other)) { throw new ReferenceError("Duplicated methods (" + element.key + ") can't be decorated."); } other.descriptor = element.descriptor; } else { if (_hasDecorators(element)) { if (_hasDecorators(other)) { throw new ReferenceError("Decorators can't be placed on different accessors with for " + "the same property (" + element.key + ")."); } other.decorators = element.decorators; } _coalesceGetterSetter(element, other); } } else { newElements.push(element); } } return newElements; } 10 | 11 | function _hasDecorators(element) { return element.decorators && element.decorators.length; } 12 | 13 | function _isDataDescriptor(desc) { return desc !== undefined && !(desc.value === undefined && desc.writable === undefined); } 14 | 15 | function _optionalCallableProperty(obj, name) { var value = obj[name]; if (value !== undefined && typeof value !== "function") { throw new TypeError("Expected '" + name + "' to be a function"); } return value; } 16 | 17 | function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } 18 | 19 | function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } 20 | 21 | function _toArray(arr) { return _arrayWithHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableRest(); } 22 | 23 | function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } 24 | 25 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } 26 | 27 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } 28 | 29 | function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); } 30 | 31 | function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } 32 | 33 | import { LitState, stateVar } from '../../../lit-state.js'; 34 | 35 | let DemoState = _decorate(null, function (_initialize, _LitState) { 36 | class DemoState extends _LitState { 37 | constructor(...args) { 38 | super(...args); 39 | 40 | _initialize(this); 41 | } 42 | 43 | } 44 | 45 | return { 46 | F: DemoState, 47 | d: [{ 48 | kind: "field", 49 | decorators: [stateVar()], 50 | key: "counter", 51 | 52 | value() { 53 | return 0; 54 | } 55 | 56 | }] 57 | }; 58 | }, LitState); 59 | 60 | export const demoState = new DemoState(); -------------------------------------------------------------------------------- /docs/build/dist/advanced-usage/manually-observe-state/observe-specific-state-vars/state.js: -------------------------------------------------------------------------------- 1 | function _decorate(decorators, factory, superClass, mixins) { var api = _getDecoratorsApi(); if (mixins) { for (var i = 0; i < mixins.length; i++) { api = mixins[i](api); } } var r = factory(function initialize(O) { api.initializeInstanceElements(O, decorated.elements); }, superClass); var decorated = api.decorateClass(_coalesceClassElements(r.d.map(_createElementDescriptor)), decorators); api.initializeClassElements(r.F, decorated.elements); return api.runClassFinishers(r.F, decorated.finishers); } 2 | 3 | function _getDecoratorsApi() { _getDecoratorsApi = function () { return api; }; var api = { elementsDefinitionOrder: [["method"], ["field"]], initializeInstanceElements: function (O, elements) { ["method", "field"].forEach(function (kind) { elements.forEach(function (element) { if (element.kind === kind && element.placement === "own") { this.defineClassElement(O, element); } }, this); }, this); }, initializeClassElements: function (F, elements) { var proto = F.prototype; ["method", "field"].forEach(function (kind) { elements.forEach(function (element) { var placement = element.placement; if (element.kind === kind && (placement === "static" || placement === "prototype")) { var receiver = placement === "static" ? F : proto; this.defineClassElement(receiver, element); } }, this); }, this); }, defineClassElement: function (receiver, element) { var descriptor = element.descriptor; if (element.kind === "field") { var initializer = element.initializer; descriptor = { enumerable: descriptor.enumerable, writable: descriptor.writable, configurable: descriptor.configurable, value: initializer === void 0 ? void 0 : initializer.call(receiver) }; } Object.defineProperty(receiver, element.key, descriptor); }, decorateClass: function (elements, decorators) { var newElements = []; var finishers = []; var placements = { static: [], prototype: [], own: [] }; elements.forEach(function (element) { this.addElementPlacement(element, placements); }, this); elements.forEach(function (element) { if (!_hasDecorators(element)) return newElements.push(element); var elementFinishersExtras = this.decorateElement(element, placements); newElements.push(elementFinishersExtras.element); newElements.push.apply(newElements, elementFinishersExtras.extras); finishers.push.apply(finishers, elementFinishersExtras.finishers); }, this); if (!decorators) { return { elements: newElements, finishers: finishers }; } var result = this.decorateConstructor(newElements, decorators); finishers.push.apply(finishers, result.finishers); result.finishers = finishers; return result; }, addElementPlacement: function (element, placements, silent) { var keys = placements[element.placement]; if (!silent && keys.indexOf(element.key) !== -1) { throw new TypeError("Duplicated element (" + element.key + ")"); } keys.push(element.key); }, decorateElement: function (element, placements) { var extras = []; var finishers = []; for (var decorators = element.decorators, i = decorators.length - 1; i >= 0; i--) { var keys = placements[element.placement]; keys.splice(keys.indexOf(element.key), 1); var elementObject = this.fromElementDescriptor(element); var elementFinisherExtras = this.toElementFinisherExtras((0, decorators[i])(elementObject) || elementObject); element = elementFinisherExtras.element; this.addElementPlacement(element, placements); if (elementFinisherExtras.finisher) { finishers.push(elementFinisherExtras.finisher); } var newExtras = elementFinisherExtras.extras; if (newExtras) { for (var j = 0; j < newExtras.length; j++) { this.addElementPlacement(newExtras[j], placements); } extras.push.apply(extras, newExtras); } } return { element: element, finishers: finishers, extras: extras }; }, decorateConstructor: function (elements, decorators) { var finishers = []; for (var i = decorators.length - 1; i >= 0; i--) { var obj = this.fromClassDescriptor(elements); var elementsAndFinisher = this.toClassDescriptor((0, decorators[i])(obj) || obj); if (elementsAndFinisher.finisher !== undefined) { finishers.push(elementsAndFinisher.finisher); } if (elementsAndFinisher.elements !== undefined) { elements = elementsAndFinisher.elements; for (var j = 0; j < elements.length - 1; j++) { for (var k = j + 1; k < elements.length; k++) { if (elements[j].key === elements[k].key && elements[j].placement === elements[k].placement) { throw new TypeError("Duplicated element (" + elements[j].key + ")"); } } } } } return { elements: elements, finishers: finishers }; }, fromElementDescriptor: function (element) { var obj = { kind: element.kind, key: element.key, placement: element.placement, descriptor: element.descriptor }; var desc = { value: "Descriptor", configurable: true }; Object.defineProperty(obj, Symbol.toStringTag, desc); if (element.kind === "field") obj.initializer = element.initializer; return obj; }, toElementDescriptors: function (elementObjects) { if (elementObjects === undefined) return; return _toArray(elementObjects).map(function (elementObject) { var element = this.toElementDescriptor(elementObject); this.disallowProperty(elementObject, "finisher", "An element descriptor"); this.disallowProperty(elementObject, "extras", "An element descriptor"); return element; }, this); }, toElementDescriptor: function (elementObject) { var kind = String(elementObject.kind); if (kind !== "method" && kind !== "field") { throw new TypeError('An element descriptor\'s .kind property must be either "method" or' + ' "field", but a decorator created an element descriptor with' + ' .kind "' + kind + '"'); } var key = _toPropertyKey(elementObject.key); var placement = String(elementObject.placement); if (placement !== "static" && placement !== "prototype" && placement !== "own") { throw new TypeError('An element descriptor\'s .placement property must be one of "static",' + ' "prototype" or "own", but a decorator created an element descriptor' + ' with .placement "' + placement + '"'); } var descriptor = elementObject.descriptor; this.disallowProperty(elementObject, "elements", "An element descriptor"); var element = { kind: kind, key: key, placement: placement, descriptor: Object.assign({}, descriptor) }; if (kind !== "field") { this.disallowProperty(elementObject, "initializer", "A method descriptor"); } else { this.disallowProperty(descriptor, "get", "The property descriptor of a field descriptor"); this.disallowProperty(descriptor, "set", "The property descriptor of a field descriptor"); this.disallowProperty(descriptor, "value", "The property descriptor of a field descriptor"); element.initializer = elementObject.initializer; } return element; }, toElementFinisherExtras: function (elementObject) { var element = this.toElementDescriptor(elementObject); var finisher = _optionalCallableProperty(elementObject, "finisher"); var extras = this.toElementDescriptors(elementObject.extras); return { element: element, finisher: finisher, extras: extras }; }, fromClassDescriptor: function (elements) { var obj = { kind: "class", elements: elements.map(this.fromElementDescriptor, this) }; var desc = { value: "Descriptor", configurable: true }; Object.defineProperty(obj, Symbol.toStringTag, desc); return obj; }, toClassDescriptor: function (obj) { var kind = String(obj.kind); if (kind !== "class") { throw new TypeError('A class descriptor\'s .kind property must be "class", but a decorator' + ' created a class descriptor with .kind "' + kind + '"'); } this.disallowProperty(obj, "key", "A class descriptor"); this.disallowProperty(obj, "placement", "A class descriptor"); this.disallowProperty(obj, "descriptor", "A class descriptor"); this.disallowProperty(obj, "initializer", "A class descriptor"); this.disallowProperty(obj, "extras", "A class descriptor"); var finisher = _optionalCallableProperty(obj, "finisher"); var elements = this.toElementDescriptors(obj.elements); return { elements: elements, finisher: finisher }; }, runClassFinishers: function (constructor, finishers) { for (var i = 0; i < finishers.length; i++) { var newConstructor = (0, finishers[i])(constructor); if (newConstructor !== undefined) { if (typeof newConstructor !== "function") { throw new TypeError("Finishers must return a constructor."); } constructor = newConstructor; } } return constructor; }, disallowProperty: function (obj, name, objectType) { if (obj[name] !== undefined) { throw new TypeError(objectType + " can't have a ." + name + " property."); } } }; return api; } 4 | 5 | function _createElementDescriptor(def) { var key = _toPropertyKey(def.key); var descriptor; if (def.kind === "method") { descriptor = { value: def.value, writable: true, configurable: true, enumerable: false }; } else if (def.kind === "get") { descriptor = { get: def.value, configurable: true, enumerable: false }; } else if (def.kind === "set") { descriptor = { set: def.value, configurable: true, enumerable: false }; } else if (def.kind === "field") { descriptor = { configurable: true, writable: true, enumerable: true }; } var element = { kind: def.kind === "field" ? "field" : "method", key: key, placement: def.static ? "static" : def.kind === "field" ? "own" : "prototype", descriptor: descriptor }; if (def.decorators) element.decorators = def.decorators; if (def.kind === "field") element.initializer = def.value; return element; } 6 | 7 | function _coalesceGetterSetter(element, other) { if (element.descriptor.get !== undefined) { other.descriptor.get = element.descriptor.get; } else { other.descriptor.set = element.descriptor.set; } } 8 | 9 | function _coalesceClassElements(elements) { var newElements = []; var isSameElement = function (other) { return other.kind === "method" && other.key === element.key && other.placement === element.placement; }; for (var i = 0; i < elements.length; i++) { var element = elements[i]; var other; if (element.kind === "method" && (other = newElements.find(isSameElement))) { if (_isDataDescriptor(element.descriptor) || _isDataDescriptor(other.descriptor)) { if (_hasDecorators(element) || _hasDecorators(other)) { throw new ReferenceError("Duplicated methods (" + element.key + ") can't be decorated."); } other.descriptor = element.descriptor; } else { if (_hasDecorators(element)) { if (_hasDecorators(other)) { throw new ReferenceError("Decorators can't be placed on different accessors with for " + "the same property (" + element.key + ")."); } other.decorators = element.decorators; } _coalesceGetterSetter(element, other); } } else { newElements.push(element); } } return newElements; } 10 | 11 | function _hasDecorators(element) { return element.decorators && element.decorators.length; } 12 | 13 | function _isDataDescriptor(desc) { return desc !== undefined && !(desc.value === undefined && desc.writable === undefined); } 14 | 15 | function _optionalCallableProperty(obj, name) { var value = obj[name]; if (value !== undefined && typeof value !== "function") { throw new TypeError("Expected '" + name + "' to be a function"); } return value; } 16 | 17 | function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } 18 | 19 | function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } 20 | 21 | function _toArray(arr) { return _arrayWithHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableRest(); } 22 | 23 | function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } 24 | 25 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } 26 | 27 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } 28 | 29 | function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); } 30 | 31 | function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } 32 | 33 | import { LitState, stateVar } from '../../../lit-state.js'; 34 | 35 | let DemoState = _decorate(null, function (_initialize, _LitState) { 36 | class DemoState extends _LitState { 37 | constructor(...args) { 38 | super(...args); 39 | 40 | _initialize(this); 41 | } 42 | 43 | } 44 | 45 | return { 46 | F: DemoState, 47 | d: [{ 48 | kind: "field", 49 | decorators: [stateVar()], 50 | key: "counter1", 51 | 52 | value() { 53 | return 0; 54 | } 55 | 56 | }, { 57 | kind: "field", 58 | decorators: [stateVar()], 59 | key: "counter2", 60 | 61 | value() { 62 | return 0; 63 | } 64 | 65 | }] 66 | }; 67 | }, LitState); 68 | 69 | export const demoState = new DemoState(); -------------------------------------------------------------------------------- /docs/build/dist/basic-usage/no-decorator-usage/state.js: -------------------------------------------------------------------------------- 1 | import { LitState } from '../../lit-state.js'; 2 | 3 | class DemoState extends LitState { 4 | static get stateVars() { 5 | return { 6 | counter: 0 7 | }; 8 | } 9 | 10 | } 11 | 12 | export const demoState = new DemoState(); -------------------------------------------------------------------------------- /docs/build/dist/basic-usage/state.js: -------------------------------------------------------------------------------- 1 | function _decorate(decorators, factory, superClass, mixins) { var api = _getDecoratorsApi(); if (mixins) { for (var i = 0; i < mixins.length; i++) { api = mixins[i](api); } } var r = factory(function initialize(O) { api.initializeInstanceElements(O, decorated.elements); }, superClass); var decorated = api.decorateClass(_coalesceClassElements(r.d.map(_createElementDescriptor)), decorators); api.initializeClassElements(r.F, decorated.elements); return api.runClassFinishers(r.F, decorated.finishers); } 2 | 3 | function _getDecoratorsApi() { _getDecoratorsApi = function () { return api; }; var api = { elementsDefinitionOrder: [["method"], ["field"]], initializeInstanceElements: function (O, elements) { ["method", "field"].forEach(function (kind) { elements.forEach(function (element) { if (element.kind === kind && element.placement === "own") { this.defineClassElement(O, element); } }, this); }, this); }, initializeClassElements: function (F, elements) { var proto = F.prototype; ["method", "field"].forEach(function (kind) { elements.forEach(function (element) { var placement = element.placement; if (element.kind === kind && (placement === "static" || placement === "prototype")) { var receiver = placement === "static" ? F : proto; this.defineClassElement(receiver, element); } }, this); }, this); }, defineClassElement: function (receiver, element) { var descriptor = element.descriptor; if (element.kind === "field") { var initializer = element.initializer; descriptor = { enumerable: descriptor.enumerable, writable: descriptor.writable, configurable: descriptor.configurable, value: initializer === void 0 ? void 0 : initializer.call(receiver) }; } Object.defineProperty(receiver, element.key, descriptor); }, decorateClass: function (elements, decorators) { var newElements = []; var finishers = []; var placements = { static: [], prototype: [], own: [] }; elements.forEach(function (element) { this.addElementPlacement(element, placements); }, this); elements.forEach(function (element) { if (!_hasDecorators(element)) return newElements.push(element); var elementFinishersExtras = this.decorateElement(element, placements); newElements.push(elementFinishersExtras.element); newElements.push.apply(newElements, elementFinishersExtras.extras); finishers.push.apply(finishers, elementFinishersExtras.finishers); }, this); if (!decorators) { return { elements: newElements, finishers: finishers }; } var result = this.decorateConstructor(newElements, decorators); finishers.push.apply(finishers, result.finishers); result.finishers = finishers; return result; }, addElementPlacement: function (element, placements, silent) { var keys = placements[element.placement]; if (!silent && keys.indexOf(element.key) !== -1) { throw new TypeError("Duplicated element (" + element.key + ")"); } keys.push(element.key); }, decorateElement: function (element, placements) { var extras = []; var finishers = []; for (var decorators = element.decorators, i = decorators.length - 1; i >= 0; i--) { var keys = placements[element.placement]; keys.splice(keys.indexOf(element.key), 1); var elementObject = this.fromElementDescriptor(element); var elementFinisherExtras = this.toElementFinisherExtras((0, decorators[i])(elementObject) || elementObject); element = elementFinisherExtras.element; this.addElementPlacement(element, placements); if (elementFinisherExtras.finisher) { finishers.push(elementFinisherExtras.finisher); } var newExtras = elementFinisherExtras.extras; if (newExtras) { for (var j = 0; j < newExtras.length; j++) { this.addElementPlacement(newExtras[j], placements); } extras.push.apply(extras, newExtras); } } return { element: element, finishers: finishers, extras: extras }; }, decorateConstructor: function (elements, decorators) { var finishers = []; for (var i = decorators.length - 1; i >= 0; i--) { var obj = this.fromClassDescriptor(elements); var elementsAndFinisher = this.toClassDescriptor((0, decorators[i])(obj) || obj); if (elementsAndFinisher.finisher !== undefined) { finishers.push(elementsAndFinisher.finisher); } if (elementsAndFinisher.elements !== undefined) { elements = elementsAndFinisher.elements; for (var j = 0; j < elements.length - 1; j++) { for (var k = j + 1; k < elements.length; k++) { if (elements[j].key === elements[k].key && elements[j].placement === elements[k].placement) { throw new TypeError("Duplicated element (" + elements[j].key + ")"); } } } } } return { elements: elements, finishers: finishers }; }, fromElementDescriptor: function (element) { var obj = { kind: element.kind, key: element.key, placement: element.placement, descriptor: element.descriptor }; var desc = { value: "Descriptor", configurable: true }; Object.defineProperty(obj, Symbol.toStringTag, desc); if (element.kind === "field") obj.initializer = element.initializer; return obj; }, toElementDescriptors: function (elementObjects) { if (elementObjects === undefined) return; return _toArray(elementObjects).map(function (elementObject) { var element = this.toElementDescriptor(elementObject); this.disallowProperty(elementObject, "finisher", "An element descriptor"); this.disallowProperty(elementObject, "extras", "An element descriptor"); return element; }, this); }, toElementDescriptor: function (elementObject) { var kind = String(elementObject.kind); if (kind !== "method" && kind !== "field") { throw new TypeError('An element descriptor\'s .kind property must be either "method" or' + ' "field", but a decorator created an element descriptor with' + ' .kind "' + kind + '"'); } var key = _toPropertyKey(elementObject.key); var placement = String(elementObject.placement); if (placement !== "static" && placement !== "prototype" && placement !== "own") { throw new TypeError('An element descriptor\'s .placement property must be one of "static",' + ' "prototype" or "own", but a decorator created an element descriptor' + ' with .placement "' + placement + '"'); } var descriptor = elementObject.descriptor; this.disallowProperty(elementObject, "elements", "An element descriptor"); var element = { kind: kind, key: key, placement: placement, descriptor: Object.assign({}, descriptor) }; if (kind !== "field") { this.disallowProperty(elementObject, "initializer", "A method descriptor"); } else { this.disallowProperty(descriptor, "get", "The property descriptor of a field descriptor"); this.disallowProperty(descriptor, "set", "The property descriptor of a field descriptor"); this.disallowProperty(descriptor, "value", "The property descriptor of a field descriptor"); element.initializer = elementObject.initializer; } return element; }, toElementFinisherExtras: function (elementObject) { var element = this.toElementDescriptor(elementObject); var finisher = _optionalCallableProperty(elementObject, "finisher"); var extras = this.toElementDescriptors(elementObject.extras); return { element: element, finisher: finisher, extras: extras }; }, fromClassDescriptor: function (elements) { var obj = { kind: "class", elements: elements.map(this.fromElementDescriptor, this) }; var desc = { value: "Descriptor", configurable: true }; Object.defineProperty(obj, Symbol.toStringTag, desc); return obj; }, toClassDescriptor: function (obj) { var kind = String(obj.kind); if (kind !== "class") { throw new TypeError('A class descriptor\'s .kind property must be "class", but a decorator' + ' created a class descriptor with .kind "' + kind + '"'); } this.disallowProperty(obj, "key", "A class descriptor"); this.disallowProperty(obj, "placement", "A class descriptor"); this.disallowProperty(obj, "descriptor", "A class descriptor"); this.disallowProperty(obj, "initializer", "A class descriptor"); this.disallowProperty(obj, "extras", "A class descriptor"); var finisher = _optionalCallableProperty(obj, "finisher"); var elements = this.toElementDescriptors(obj.elements); return { elements: elements, finisher: finisher }; }, runClassFinishers: function (constructor, finishers) { for (var i = 0; i < finishers.length; i++) { var newConstructor = (0, finishers[i])(constructor); if (newConstructor !== undefined) { if (typeof newConstructor !== "function") { throw new TypeError("Finishers must return a constructor."); } constructor = newConstructor; } } return constructor; }, disallowProperty: function (obj, name, objectType) { if (obj[name] !== undefined) { throw new TypeError(objectType + " can't have a ." + name + " property."); } } }; return api; } 4 | 5 | function _createElementDescriptor(def) { var key = _toPropertyKey(def.key); var descriptor; if (def.kind === "method") { descriptor = { value: def.value, writable: true, configurable: true, enumerable: false }; } else if (def.kind === "get") { descriptor = { get: def.value, configurable: true, enumerable: false }; } else if (def.kind === "set") { descriptor = { set: def.value, configurable: true, enumerable: false }; } else if (def.kind === "field") { descriptor = { configurable: true, writable: true, enumerable: true }; } var element = { kind: def.kind === "field" ? "field" : "method", key: key, placement: def.static ? "static" : def.kind === "field" ? "own" : "prototype", descriptor: descriptor }; if (def.decorators) element.decorators = def.decorators; if (def.kind === "field") element.initializer = def.value; return element; } 6 | 7 | function _coalesceGetterSetter(element, other) { if (element.descriptor.get !== undefined) { other.descriptor.get = element.descriptor.get; } else { other.descriptor.set = element.descriptor.set; } } 8 | 9 | function _coalesceClassElements(elements) { var newElements = []; var isSameElement = function (other) { return other.kind === "method" && other.key === element.key && other.placement === element.placement; }; for (var i = 0; i < elements.length; i++) { var element = elements[i]; var other; if (element.kind === "method" && (other = newElements.find(isSameElement))) { if (_isDataDescriptor(element.descriptor) || _isDataDescriptor(other.descriptor)) { if (_hasDecorators(element) || _hasDecorators(other)) { throw new ReferenceError("Duplicated methods (" + element.key + ") can't be decorated."); } other.descriptor = element.descriptor; } else { if (_hasDecorators(element)) { if (_hasDecorators(other)) { throw new ReferenceError("Decorators can't be placed on different accessors with for " + "the same property (" + element.key + ")."); } other.decorators = element.decorators; } _coalesceGetterSetter(element, other); } } else { newElements.push(element); } } return newElements; } 10 | 11 | function _hasDecorators(element) { return element.decorators && element.decorators.length; } 12 | 13 | function _isDataDescriptor(desc) { return desc !== undefined && !(desc.value === undefined && desc.writable === undefined); } 14 | 15 | function _optionalCallableProperty(obj, name) { var value = obj[name]; if (value !== undefined && typeof value !== "function") { throw new TypeError("Expected '" + name + "' to be a function"); } return value; } 16 | 17 | function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } 18 | 19 | function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } 20 | 21 | function _toArray(arr) { return _arrayWithHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableRest(); } 22 | 23 | function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } 24 | 25 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } 26 | 27 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } 28 | 29 | function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); } 30 | 31 | function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } 32 | 33 | import { LitState, stateVar } from '../lit-state.js'; 34 | 35 | let DemoState = _decorate(null, function (_initialize, _LitState) { 36 | class DemoState extends _LitState { 37 | constructor(...args) { 38 | super(...args); 39 | 40 | _initialize(this); 41 | } 42 | 43 | } 44 | 45 | return { 46 | F: DemoState, 47 | d: [{ 48 | kind: "field", 49 | decorators: [stateVar()], 50 | key: "counter", 51 | 52 | value() { 53 | return 0; 54 | } 55 | 56 | }] 57 | }; 58 | }, LitState); 59 | 60 | export const demoState = new DemoState(); -------------------------------------------------------------------------------- /docs/build/dist/demo-component.js: -------------------------------------------------------------------------------- 1 | import { css } from '../web_modules/lit-element.js'; 2 | import { litStyle } from '../web_modules/lit-element-style.js'; 3 | import '../web_modules/lit-docs.js'; 4 | export const demoComponentStyle = litStyle(css` 5 | 6 | h2 { 7 | margin-top: 0; 8 | font-size: 20px; 9 | color: green; 10 | } 11 | 12 | h3 { 13 | margin: 20px 0; 14 | font-weight: 600; 15 | font-size: 16px; 16 | } 17 | 18 | .status { 19 | color: blue; 20 | } 21 | 22 | .value { 23 | color: #cb2828; 24 | } 25 | 26 | .buttons { 27 | display: flex; 28 | flex-wrap: wrap; 29 | margin: -5px 0 0 -5px; 30 | } 31 | 32 | .buttons > * { 33 | margin: 5px 0 0 5px; 34 | } 35 | 36 | `); -------------------------------------------------------------------------------- /docs/build/dist/lit-state.js: -------------------------------------------------------------------------------- 1 | export const observeState = superclass => class extends superclass { 2 | constructor() { 3 | super(); 4 | this._observers = []; 5 | } 6 | 7 | update(changedProperties) { 8 | stateRecorder.start(); 9 | super.update(changedProperties); 10 | 11 | this._initStateObservers(); 12 | } 13 | 14 | connectedCallback() { 15 | super.connectedCallback(); 16 | 17 | if (this._wasConnected) { 18 | this.requestUpdate(); 19 | delete this._wasConnected; 20 | } 21 | } 22 | 23 | disconnectedCallback() { 24 | super.disconnectedCallback(); 25 | this._wasConnected = true; 26 | 27 | this._clearStateObservers(); 28 | } 29 | 30 | _initStateObservers() { 31 | this._clearStateObservers(); 32 | 33 | if (!this.isConnected) return; 34 | 35 | this._addStateObservers(stateRecorder.finish()); 36 | } 37 | 38 | _addStateObservers(stateVars) { 39 | for (let [state, keys] of stateVars) { 40 | const observer = () => this.requestUpdate(); 41 | 42 | this._observers.push([state, observer]); 43 | 44 | state.addObserver(observer, keys); 45 | } 46 | } 47 | 48 | _clearStateObservers() { 49 | for (let [state, observer] of this._observers) { 50 | state.removeObserver(observer); 51 | } 52 | 53 | this._observers = []; 54 | } 55 | 56 | }; 57 | export class LitState { 58 | constructor() { 59 | this._observers = []; 60 | 61 | this._initStateVars(); 62 | } 63 | 64 | addObserver(observer, keys) { 65 | this._observers.push({ 66 | observer, 67 | keys 68 | }); 69 | } 70 | 71 | removeObserver(observer) { 72 | this._observers = this._observers.filter(observerObj => observerObj.observer !== observer); 73 | } 74 | 75 | _initStateVars() { 76 | if (this.constructor.stateVarOptions) { 77 | for (let [key, options] of Object.entries(this.constructor.stateVarOptions)) { 78 | this._initStateVar(key, options); 79 | } 80 | } 81 | 82 | if (this.constructor.stateVars) { 83 | for (let [key, value] of Object.entries(this.constructor.stateVars)) { 84 | this._initStateVar(key, {}); 85 | 86 | this[key] = value; 87 | } 88 | } 89 | } 90 | 91 | _initStateVar(key, options) { 92 | if (this.hasOwnProperty(key)) { 93 | // Property already defined, so don't re-define. 94 | return; 95 | } 96 | 97 | options = this._parseOptions(options); 98 | const stateVar = new options.handler({ 99 | options: options, 100 | recordRead: () => this._recordRead(key), 101 | notifyChange: () => this._notifyChange(key) 102 | }); 103 | Object.defineProperty(this, key, { 104 | get() { 105 | return stateVar.get(); 106 | }, 107 | 108 | set(value) { 109 | if (stateVar.shouldSetValue(value)) { 110 | stateVar.set(value); 111 | } 112 | }, 113 | 114 | configurable: true, 115 | enumerable: true 116 | }); 117 | } 118 | 119 | _parseOptions(options) { 120 | if (!options.handler) { 121 | options.handler = StateVar; 122 | } else { 123 | // In case of a custom `StateVar` handler is provided, we offer a 124 | // second way of providing options to your custom handler class. 125 | // 126 | // You can decorate a *method* with `@stateVar()` instead of a 127 | // variable. The method must return an object, and that object will 128 | // be assigned to the `options` object. 129 | // 130 | // Within the method you have access to the `this` context. So you 131 | // can access other properties and methods from your state class. 132 | // And you can add arrow function callbacks where you can access 133 | // `this`. This provides a lot of possibilities for a custom 134 | // handler class. 135 | if (options.propertyMethod && options.propertyMethod.kind === 'method') { 136 | Object.assign(options, options.propertyMethod.descriptor.value.call(this)); 137 | } 138 | } 139 | 140 | return options; 141 | } 142 | 143 | _recordRead(key) { 144 | stateRecorder.recordRead(this, key); 145 | } 146 | 147 | _notifyChange(key) { 148 | for (const observerObj of this._observers) { 149 | if (!observerObj.keys || observerObj.keys.includes(key)) { 150 | observerObj.observer(key); 151 | } 152 | } 153 | 154 | ; 155 | } 156 | 157 | } 158 | export class StateVar { 159 | constructor(args) { 160 | this.options = args.options; // The options given in the `stateVar` declaration 161 | 162 | this.recordRead = args.recordRead; // Callback to indicate the `stateVar` is read 163 | 164 | this.notifyChange = args.notifyChange; // Callback to indicate the `stateVar` value has changed 165 | 166 | this.value = undefined; // The initial value 167 | } // Called when the `stateVar` on the `LitState` class is read (for example: 168 | // `myState.myStateVar`). Should return the value of the `stateVar`. 169 | 170 | 171 | get() { 172 | this.recordRead(); 173 | return this.value; 174 | } // Called before the `set()` method is called. If this method returns 175 | // `false`, the `set()` method won't be called. This can be used for 176 | // validation and/or optimization. 177 | 178 | 179 | shouldSetValue(value) { 180 | return this.value !== value; 181 | } // Called when the `stateVar` on the `LitState` class is set (for example: 182 | // `myState.myStateVar = 'value'`. 183 | 184 | 185 | set(value) { 186 | this.value = value; 187 | this.notifyChange(); 188 | } 189 | 190 | } 191 | export function stateVar(options = {}) { 192 | return element => { 193 | return { 194 | kind: 'field', 195 | key: Symbol(), 196 | placement: 'own', 197 | descriptor: {}, 198 | 199 | initializer() { 200 | if (typeof element.initializer === 'function') { 201 | this[element.key] = element.initializer.call(this); 202 | } 203 | }, 204 | 205 | finisher(litStateClass) { 206 | if (element.kind === 'method') { 207 | // You can decorate a *method* with `@stateVar()` instead 208 | // of a variable. When the state class is constructed, this 209 | // method will be called, and it's return value must be an 210 | // object that will be added to the options the stateVar 211 | // handler will receive. 212 | options.propertyMethod = element; 213 | } 214 | 215 | if (litStateClass.stateVarOptions === undefined) { 216 | litStateClass.stateVarOptions = {}; 217 | } 218 | 219 | litStateClass.stateVarOptions[element.key] = options; 220 | } 221 | 222 | }; 223 | }; 224 | } 225 | 226 | class StateRecorder { 227 | constructor() { 228 | this._log = null; 229 | } 230 | 231 | start() { 232 | this._log = new Map(); 233 | } 234 | 235 | recordRead(stateObj, key) { 236 | if (this._log === null) return; 237 | const keys = this._log.get(stateObj) || []; 238 | if (!keys.includes(key)) keys.push(key); 239 | 240 | this._log.set(stateObj, keys); 241 | } 242 | 243 | finish() { 244 | const stateVars = this._log; 245 | this._log = null; 246 | return stateVars; 247 | } 248 | 249 | } 250 | 251 | export const stateRecorder = new StateRecorder(); -------------------------------------------------------------------------------- /docs/build/dist/use-cases/different-vars-on-rerender/state.js: -------------------------------------------------------------------------------- 1 | function _decorate(decorators, factory, superClass, mixins) { var api = _getDecoratorsApi(); if (mixins) { for (var i = 0; i < mixins.length; i++) { api = mixins[i](api); } } var r = factory(function initialize(O) { api.initializeInstanceElements(O, decorated.elements); }, superClass); var decorated = api.decorateClass(_coalesceClassElements(r.d.map(_createElementDescriptor)), decorators); api.initializeClassElements(r.F, decorated.elements); return api.runClassFinishers(r.F, decorated.finishers); } 2 | 3 | function _getDecoratorsApi() { _getDecoratorsApi = function () { return api; }; var api = { elementsDefinitionOrder: [["method"], ["field"]], initializeInstanceElements: function (O, elements) { ["method", "field"].forEach(function (kind) { elements.forEach(function (element) { if (element.kind === kind && element.placement === "own") { this.defineClassElement(O, element); } }, this); }, this); }, initializeClassElements: function (F, elements) { var proto = F.prototype; ["method", "field"].forEach(function (kind) { elements.forEach(function (element) { var placement = element.placement; if (element.kind === kind && (placement === "static" || placement === "prototype")) { var receiver = placement === "static" ? F : proto; this.defineClassElement(receiver, element); } }, this); }, this); }, defineClassElement: function (receiver, element) { var descriptor = element.descriptor; if (element.kind === "field") { var initializer = element.initializer; descriptor = { enumerable: descriptor.enumerable, writable: descriptor.writable, configurable: descriptor.configurable, value: initializer === void 0 ? void 0 : initializer.call(receiver) }; } Object.defineProperty(receiver, element.key, descriptor); }, decorateClass: function (elements, decorators) { var newElements = []; var finishers = []; var placements = { static: [], prototype: [], own: [] }; elements.forEach(function (element) { this.addElementPlacement(element, placements); }, this); elements.forEach(function (element) { if (!_hasDecorators(element)) return newElements.push(element); var elementFinishersExtras = this.decorateElement(element, placements); newElements.push(elementFinishersExtras.element); newElements.push.apply(newElements, elementFinishersExtras.extras); finishers.push.apply(finishers, elementFinishersExtras.finishers); }, this); if (!decorators) { return { elements: newElements, finishers: finishers }; } var result = this.decorateConstructor(newElements, decorators); finishers.push.apply(finishers, result.finishers); result.finishers = finishers; return result; }, addElementPlacement: function (element, placements, silent) { var keys = placements[element.placement]; if (!silent && keys.indexOf(element.key) !== -1) { throw new TypeError("Duplicated element (" + element.key + ")"); } keys.push(element.key); }, decorateElement: function (element, placements) { var extras = []; var finishers = []; for (var decorators = element.decorators, i = decorators.length - 1; i >= 0; i--) { var keys = placements[element.placement]; keys.splice(keys.indexOf(element.key), 1); var elementObject = this.fromElementDescriptor(element); var elementFinisherExtras = this.toElementFinisherExtras((0, decorators[i])(elementObject) || elementObject); element = elementFinisherExtras.element; this.addElementPlacement(element, placements); if (elementFinisherExtras.finisher) { finishers.push(elementFinisherExtras.finisher); } var newExtras = elementFinisherExtras.extras; if (newExtras) { for (var j = 0; j < newExtras.length; j++) { this.addElementPlacement(newExtras[j], placements); } extras.push.apply(extras, newExtras); } } return { element: element, finishers: finishers, extras: extras }; }, decorateConstructor: function (elements, decorators) { var finishers = []; for (var i = decorators.length - 1; i >= 0; i--) { var obj = this.fromClassDescriptor(elements); var elementsAndFinisher = this.toClassDescriptor((0, decorators[i])(obj) || obj); if (elementsAndFinisher.finisher !== undefined) { finishers.push(elementsAndFinisher.finisher); } if (elementsAndFinisher.elements !== undefined) { elements = elementsAndFinisher.elements; for (var j = 0; j < elements.length - 1; j++) { for (var k = j + 1; k < elements.length; k++) { if (elements[j].key === elements[k].key && elements[j].placement === elements[k].placement) { throw new TypeError("Duplicated element (" + elements[j].key + ")"); } } } } } return { elements: elements, finishers: finishers }; }, fromElementDescriptor: function (element) { var obj = { kind: element.kind, key: element.key, placement: element.placement, descriptor: element.descriptor }; var desc = { value: "Descriptor", configurable: true }; Object.defineProperty(obj, Symbol.toStringTag, desc); if (element.kind === "field") obj.initializer = element.initializer; return obj; }, toElementDescriptors: function (elementObjects) { if (elementObjects === undefined) return; return _toArray(elementObjects).map(function (elementObject) { var element = this.toElementDescriptor(elementObject); this.disallowProperty(elementObject, "finisher", "An element descriptor"); this.disallowProperty(elementObject, "extras", "An element descriptor"); return element; }, this); }, toElementDescriptor: function (elementObject) { var kind = String(elementObject.kind); if (kind !== "method" && kind !== "field") { throw new TypeError('An element descriptor\'s .kind property must be either "method" or' + ' "field", but a decorator created an element descriptor with' + ' .kind "' + kind + '"'); } var key = _toPropertyKey(elementObject.key); var placement = String(elementObject.placement); if (placement !== "static" && placement !== "prototype" && placement !== "own") { throw new TypeError('An element descriptor\'s .placement property must be one of "static",' + ' "prototype" or "own", but a decorator created an element descriptor' + ' with .placement "' + placement + '"'); } var descriptor = elementObject.descriptor; this.disallowProperty(elementObject, "elements", "An element descriptor"); var element = { kind: kind, key: key, placement: placement, descriptor: Object.assign({}, descriptor) }; if (kind !== "field") { this.disallowProperty(elementObject, "initializer", "A method descriptor"); } else { this.disallowProperty(descriptor, "get", "The property descriptor of a field descriptor"); this.disallowProperty(descriptor, "set", "The property descriptor of a field descriptor"); this.disallowProperty(descriptor, "value", "The property descriptor of a field descriptor"); element.initializer = elementObject.initializer; } return element; }, toElementFinisherExtras: function (elementObject) { var element = this.toElementDescriptor(elementObject); var finisher = _optionalCallableProperty(elementObject, "finisher"); var extras = this.toElementDescriptors(elementObject.extras); return { element: element, finisher: finisher, extras: extras }; }, fromClassDescriptor: function (elements) { var obj = { kind: "class", elements: elements.map(this.fromElementDescriptor, this) }; var desc = { value: "Descriptor", configurable: true }; Object.defineProperty(obj, Symbol.toStringTag, desc); return obj; }, toClassDescriptor: function (obj) { var kind = String(obj.kind); if (kind !== "class") { throw new TypeError('A class descriptor\'s .kind property must be "class", but a decorator' + ' created a class descriptor with .kind "' + kind + '"'); } this.disallowProperty(obj, "key", "A class descriptor"); this.disallowProperty(obj, "placement", "A class descriptor"); this.disallowProperty(obj, "descriptor", "A class descriptor"); this.disallowProperty(obj, "initializer", "A class descriptor"); this.disallowProperty(obj, "extras", "A class descriptor"); var finisher = _optionalCallableProperty(obj, "finisher"); var elements = this.toElementDescriptors(obj.elements); return { elements: elements, finisher: finisher }; }, runClassFinishers: function (constructor, finishers) { for (var i = 0; i < finishers.length; i++) { var newConstructor = (0, finishers[i])(constructor); if (newConstructor !== undefined) { if (typeof newConstructor !== "function") { throw new TypeError("Finishers must return a constructor."); } constructor = newConstructor; } } return constructor; }, disallowProperty: function (obj, name, objectType) { if (obj[name] !== undefined) { throw new TypeError(objectType + " can't have a ." + name + " property."); } } }; return api; } 4 | 5 | function _createElementDescriptor(def) { var key = _toPropertyKey(def.key); var descriptor; if (def.kind === "method") { descriptor = { value: def.value, writable: true, configurable: true, enumerable: false }; } else if (def.kind === "get") { descriptor = { get: def.value, configurable: true, enumerable: false }; } else if (def.kind === "set") { descriptor = { set: def.value, configurable: true, enumerable: false }; } else if (def.kind === "field") { descriptor = { configurable: true, writable: true, enumerable: true }; } var element = { kind: def.kind === "field" ? "field" : "method", key: key, placement: def.static ? "static" : def.kind === "field" ? "own" : "prototype", descriptor: descriptor }; if (def.decorators) element.decorators = def.decorators; if (def.kind === "field") element.initializer = def.value; return element; } 6 | 7 | function _coalesceGetterSetter(element, other) { if (element.descriptor.get !== undefined) { other.descriptor.get = element.descriptor.get; } else { other.descriptor.set = element.descriptor.set; } } 8 | 9 | function _coalesceClassElements(elements) { var newElements = []; var isSameElement = function (other) { return other.kind === "method" && other.key === element.key && other.placement === element.placement; }; for (var i = 0; i < elements.length; i++) { var element = elements[i]; var other; if (element.kind === "method" && (other = newElements.find(isSameElement))) { if (_isDataDescriptor(element.descriptor) || _isDataDescriptor(other.descriptor)) { if (_hasDecorators(element) || _hasDecorators(other)) { throw new ReferenceError("Duplicated methods (" + element.key + ") can't be decorated."); } other.descriptor = element.descriptor; } else { if (_hasDecorators(element)) { if (_hasDecorators(other)) { throw new ReferenceError("Decorators can't be placed on different accessors with for " + "the same property (" + element.key + ")."); } other.decorators = element.decorators; } _coalesceGetterSetter(element, other); } } else { newElements.push(element); } } return newElements; } 10 | 11 | function _hasDecorators(element) { return element.decorators && element.decorators.length; } 12 | 13 | function _isDataDescriptor(desc) { return desc !== undefined && !(desc.value === undefined && desc.writable === undefined); } 14 | 15 | function _optionalCallableProperty(obj, name) { var value = obj[name]; if (value !== undefined && typeof value !== "function") { throw new TypeError("Expected '" + name + "' to be a function"); } return value; } 16 | 17 | function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } 18 | 19 | function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } 20 | 21 | function _toArray(arr) { return _arrayWithHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableRest(); } 22 | 23 | function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } 24 | 25 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } 26 | 27 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } 28 | 29 | function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); } 30 | 31 | function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } 32 | 33 | import { LitState, stateVar } from '../../lit-state.js'; 34 | 35 | let DemoState = _decorate(null, function (_initialize, _LitState) { 36 | class DemoState extends _LitState { 37 | constructor(...args) { 38 | super(...args); 39 | 40 | _initialize(this); 41 | } 42 | 43 | } 44 | 45 | return { 46 | F: DemoState, 47 | d: [{ 48 | kind: "field", 49 | decorators: [stateVar()], 50 | key: "showCounter", 51 | 52 | value() { 53 | return 1; 54 | } 55 | 56 | }, { 57 | kind: "field", 58 | decorators: [stateVar()], 59 | key: "counter1", 60 | 61 | value() { 62 | return 0; 63 | } 64 | 65 | }, { 66 | kind: "field", 67 | decorators: [stateVar()], 68 | key: "counter2", 69 | 70 | value() { 71 | return 0; 72 | } 73 | 74 | }] 75 | }; 76 | }, LitState); 77 | 78 | export const demoState = new DemoState(); -------------------------------------------------------------------------------- /docs/build/dist/use-cases/reconnected-components/state.js: -------------------------------------------------------------------------------- 1 | function _decorate(decorators, factory, superClass, mixins) { var api = _getDecoratorsApi(); if (mixins) { for (var i = 0; i < mixins.length; i++) { api = mixins[i](api); } } var r = factory(function initialize(O) { api.initializeInstanceElements(O, decorated.elements); }, superClass); var decorated = api.decorateClass(_coalesceClassElements(r.d.map(_createElementDescriptor)), decorators); api.initializeClassElements(r.F, decorated.elements); return api.runClassFinishers(r.F, decorated.finishers); } 2 | 3 | function _getDecoratorsApi() { _getDecoratorsApi = function () { return api; }; var api = { elementsDefinitionOrder: [["method"], ["field"]], initializeInstanceElements: function (O, elements) { ["method", "field"].forEach(function (kind) { elements.forEach(function (element) { if (element.kind === kind && element.placement === "own") { this.defineClassElement(O, element); } }, this); }, this); }, initializeClassElements: function (F, elements) { var proto = F.prototype; ["method", "field"].forEach(function (kind) { elements.forEach(function (element) { var placement = element.placement; if (element.kind === kind && (placement === "static" || placement === "prototype")) { var receiver = placement === "static" ? F : proto; this.defineClassElement(receiver, element); } }, this); }, this); }, defineClassElement: function (receiver, element) { var descriptor = element.descriptor; if (element.kind === "field") { var initializer = element.initializer; descriptor = { enumerable: descriptor.enumerable, writable: descriptor.writable, configurable: descriptor.configurable, value: initializer === void 0 ? void 0 : initializer.call(receiver) }; } Object.defineProperty(receiver, element.key, descriptor); }, decorateClass: function (elements, decorators) { var newElements = []; var finishers = []; var placements = { static: [], prototype: [], own: [] }; elements.forEach(function (element) { this.addElementPlacement(element, placements); }, this); elements.forEach(function (element) { if (!_hasDecorators(element)) return newElements.push(element); var elementFinishersExtras = this.decorateElement(element, placements); newElements.push(elementFinishersExtras.element); newElements.push.apply(newElements, elementFinishersExtras.extras); finishers.push.apply(finishers, elementFinishersExtras.finishers); }, this); if (!decorators) { return { elements: newElements, finishers: finishers }; } var result = this.decorateConstructor(newElements, decorators); finishers.push.apply(finishers, result.finishers); result.finishers = finishers; return result; }, addElementPlacement: function (element, placements, silent) { var keys = placements[element.placement]; if (!silent && keys.indexOf(element.key) !== -1) { throw new TypeError("Duplicated element (" + element.key + ")"); } keys.push(element.key); }, decorateElement: function (element, placements) { var extras = []; var finishers = []; for (var decorators = element.decorators, i = decorators.length - 1; i >= 0; i--) { var keys = placements[element.placement]; keys.splice(keys.indexOf(element.key), 1); var elementObject = this.fromElementDescriptor(element); var elementFinisherExtras = this.toElementFinisherExtras((0, decorators[i])(elementObject) || elementObject); element = elementFinisherExtras.element; this.addElementPlacement(element, placements); if (elementFinisherExtras.finisher) { finishers.push(elementFinisherExtras.finisher); } var newExtras = elementFinisherExtras.extras; if (newExtras) { for (var j = 0; j < newExtras.length; j++) { this.addElementPlacement(newExtras[j], placements); } extras.push.apply(extras, newExtras); } } return { element: element, finishers: finishers, extras: extras }; }, decorateConstructor: function (elements, decorators) { var finishers = []; for (var i = decorators.length - 1; i >= 0; i--) { var obj = this.fromClassDescriptor(elements); var elementsAndFinisher = this.toClassDescriptor((0, decorators[i])(obj) || obj); if (elementsAndFinisher.finisher !== undefined) { finishers.push(elementsAndFinisher.finisher); } if (elementsAndFinisher.elements !== undefined) { elements = elementsAndFinisher.elements; for (var j = 0; j < elements.length - 1; j++) { for (var k = j + 1; k < elements.length; k++) { if (elements[j].key === elements[k].key && elements[j].placement === elements[k].placement) { throw new TypeError("Duplicated element (" + elements[j].key + ")"); } } } } } return { elements: elements, finishers: finishers }; }, fromElementDescriptor: function (element) { var obj = { kind: element.kind, key: element.key, placement: element.placement, descriptor: element.descriptor }; var desc = { value: "Descriptor", configurable: true }; Object.defineProperty(obj, Symbol.toStringTag, desc); if (element.kind === "field") obj.initializer = element.initializer; return obj; }, toElementDescriptors: function (elementObjects) { if (elementObjects === undefined) return; return _toArray(elementObjects).map(function (elementObject) { var element = this.toElementDescriptor(elementObject); this.disallowProperty(elementObject, "finisher", "An element descriptor"); this.disallowProperty(elementObject, "extras", "An element descriptor"); return element; }, this); }, toElementDescriptor: function (elementObject) { var kind = String(elementObject.kind); if (kind !== "method" && kind !== "field") { throw new TypeError('An element descriptor\'s .kind property must be either "method" or' + ' "field", but a decorator created an element descriptor with' + ' .kind "' + kind + '"'); } var key = _toPropertyKey(elementObject.key); var placement = String(elementObject.placement); if (placement !== "static" && placement !== "prototype" && placement !== "own") { throw new TypeError('An element descriptor\'s .placement property must be one of "static",' + ' "prototype" or "own", but a decorator created an element descriptor' + ' with .placement "' + placement + '"'); } var descriptor = elementObject.descriptor; this.disallowProperty(elementObject, "elements", "An element descriptor"); var element = { kind: kind, key: key, placement: placement, descriptor: Object.assign({}, descriptor) }; if (kind !== "field") { this.disallowProperty(elementObject, "initializer", "A method descriptor"); } else { this.disallowProperty(descriptor, "get", "The property descriptor of a field descriptor"); this.disallowProperty(descriptor, "set", "The property descriptor of a field descriptor"); this.disallowProperty(descriptor, "value", "The property descriptor of a field descriptor"); element.initializer = elementObject.initializer; } return element; }, toElementFinisherExtras: function (elementObject) { var element = this.toElementDescriptor(elementObject); var finisher = _optionalCallableProperty(elementObject, "finisher"); var extras = this.toElementDescriptors(elementObject.extras); return { element: element, finisher: finisher, extras: extras }; }, fromClassDescriptor: function (elements) { var obj = { kind: "class", elements: elements.map(this.fromElementDescriptor, this) }; var desc = { value: "Descriptor", configurable: true }; Object.defineProperty(obj, Symbol.toStringTag, desc); return obj; }, toClassDescriptor: function (obj) { var kind = String(obj.kind); if (kind !== "class") { throw new TypeError('A class descriptor\'s .kind property must be "class", but a decorator' + ' created a class descriptor with .kind "' + kind + '"'); } this.disallowProperty(obj, "key", "A class descriptor"); this.disallowProperty(obj, "placement", "A class descriptor"); this.disallowProperty(obj, "descriptor", "A class descriptor"); this.disallowProperty(obj, "initializer", "A class descriptor"); this.disallowProperty(obj, "extras", "A class descriptor"); var finisher = _optionalCallableProperty(obj, "finisher"); var elements = this.toElementDescriptors(obj.elements); return { elements: elements, finisher: finisher }; }, runClassFinishers: function (constructor, finishers) { for (var i = 0; i < finishers.length; i++) { var newConstructor = (0, finishers[i])(constructor); if (newConstructor !== undefined) { if (typeof newConstructor !== "function") { throw new TypeError("Finishers must return a constructor."); } constructor = newConstructor; } } return constructor; }, disallowProperty: function (obj, name, objectType) { if (obj[name] !== undefined) { throw new TypeError(objectType + " can't have a ." + name + " property."); } } }; return api; } 4 | 5 | function _createElementDescriptor(def) { var key = _toPropertyKey(def.key); var descriptor; if (def.kind === "method") { descriptor = { value: def.value, writable: true, configurable: true, enumerable: false }; } else if (def.kind === "get") { descriptor = { get: def.value, configurable: true, enumerable: false }; } else if (def.kind === "set") { descriptor = { set: def.value, configurable: true, enumerable: false }; } else if (def.kind === "field") { descriptor = { configurable: true, writable: true, enumerable: true }; } var element = { kind: def.kind === "field" ? "field" : "method", key: key, placement: def.static ? "static" : def.kind === "field" ? "own" : "prototype", descriptor: descriptor }; if (def.decorators) element.decorators = def.decorators; if (def.kind === "field") element.initializer = def.value; return element; } 6 | 7 | function _coalesceGetterSetter(element, other) { if (element.descriptor.get !== undefined) { other.descriptor.get = element.descriptor.get; } else { other.descriptor.set = element.descriptor.set; } } 8 | 9 | function _coalesceClassElements(elements) { var newElements = []; var isSameElement = function (other) { return other.kind === "method" && other.key === element.key && other.placement === element.placement; }; for (var i = 0; i < elements.length; i++) { var element = elements[i]; var other; if (element.kind === "method" && (other = newElements.find(isSameElement))) { if (_isDataDescriptor(element.descriptor) || _isDataDescriptor(other.descriptor)) { if (_hasDecorators(element) || _hasDecorators(other)) { throw new ReferenceError("Duplicated methods (" + element.key + ") can't be decorated."); } other.descriptor = element.descriptor; } else { if (_hasDecorators(element)) { if (_hasDecorators(other)) { throw new ReferenceError("Decorators can't be placed on different accessors with for " + "the same property (" + element.key + ")."); } other.decorators = element.decorators; } _coalesceGetterSetter(element, other); } } else { newElements.push(element); } } return newElements; } 10 | 11 | function _hasDecorators(element) { return element.decorators && element.decorators.length; } 12 | 13 | function _isDataDescriptor(desc) { return desc !== undefined && !(desc.value === undefined && desc.writable === undefined); } 14 | 15 | function _optionalCallableProperty(obj, name) { var value = obj[name]; if (value !== undefined && typeof value !== "function") { throw new TypeError("Expected '" + name + "' to be a function"); } return value; } 16 | 17 | function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } 18 | 19 | function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } 20 | 21 | function _toArray(arr) { return _arrayWithHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableRest(); } 22 | 23 | function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } 24 | 25 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } 26 | 27 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } 28 | 29 | function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); } 30 | 31 | function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } 32 | 33 | import { LitState, stateVar } from '../../lit-state.js'; 34 | 35 | let DemoState = _decorate(null, function (_initialize, _LitState) { 36 | class DemoState extends _LitState { 37 | constructor(...args) { 38 | super(...args); 39 | 40 | _initialize(this); 41 | } 42 | 43 | } 44 | 45 | return { 46 | F: DemoState, 47 | d: [{ 48 | kind: "field", 49 | decorators: [stateVar()], 50 | key: "counter", 51 | 52 | value() { 53 | return 0; 54 | } 55 | 56 | }, { 57 | kind: "field", 58 | decorators: [stateVar()], 59 | key: "connected", 60 | 61 | value() { 62 | return true; 63 | } 64 | 65 | }] 66 | }; 67 | }, LitState); 68 | 69 | export const demoState = new DemoState(); -------------------------------------------------------------------------------- /docs/build/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitaarik/lit-state/8cd66223612c3b115c0275f58f6cee5e900ee534/docs/build/favicon.png -------------------------------------------------------------------------------- /docs/build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | LitState Docs 9 | 10 | 11 | 12 | 13 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/build/snowpack/env.js: -------------------------------------------------------------------------------- 1 | export default {"MODE":"production","NODE_ENV":"production","SSR":false}; -------------------------------------------------------------------------------- /docs/build/web_modules/common/lit-style-ca615be5.js: -------------------------------------------------------------------------------- 1 | import './lit-element-336867a2.js'; 2 | 3 | function litStyle(myStyles) { 4 | 5 | return superclass => class extends superclass { 6 | 7 | static getStyles() { 8 | 9 | const styles = super.getStyles(); 10 | 11 | if (!styles) { 12 | return myStyles; 13 | } else if (Array.isArray(styles)) { 14 | return [myStyles, ...styles]; 15 | } else { 16 | return [myStyles, styles]; 17 | } 18 | 19 | } 20 | 21 | } 22 | 23 | } 24 | 25 | export { litStyle as l }; 26 | -------------------------------------------------------------------------------- /docs/build/web_modules/import-map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "lit-docs": "./lit-docs.js", 4 | "lit-element": "./lit-element.js", 5 | "lit-element-style": "./lit-element-style.js" 6 | } 7 | } -------------------------------------------------------------------------------- /docs/build/web_modules/lit-element-style.js: -------------------------------------------------------------------------------- 1 | import './common/lit-element-336867a2.js'; 2 | export { l as litStyle } from './common/lit-style-ca615be5.js'; 3 | -------------------------------------------------------------------------------- /docs/build/web_modules/lit-element.js: -------------------------------------------------------------------------------- 1 | export { L as LitElement, c as css, h as html } from './common/lit-element-336867a2.js'; 2 | 3 | /** 4 | * @license 5 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 6 | * This code may only be used under the BSD style license found at 7 | * http://polymer.github.io/LICENSE.txt 8 | * The complete set of authors may be found at 9 | * http://polymer.github.io/AUTHORS.txt 10 | * The complete set of contributors may be found at 11 | * http://polymer.github.io/CONTRIBUTORS.txt 12 | * Code distributed by Google as part of the polymer project is also 13 | * subject to an additional IP rights grant found at 14 | * http://polymer.github.io/PATENTS.txt 15 | */ 16 | const legacyCustomElement = (tagName, clazz) => { 17 | window.customElements.define(tagName, clazz); 18 | // Cast as any because TS doesn't recognize the return type as being a 19 | // subtype of the decorated class when clazz is typed as 20 | // `Constructor` for some reason. 21 | // `Constructor` is helpful to make sure the decorator is 22 | // applied to elements however. 23 | // tslint:disable-next-line:no-any 24 | return clazz; 25 | }; 26 | const standardCustomElement = (tagName, descriptor) => { 27 | const { kind, elements } = descriptor; 28 | return { 29 | kind, 30 | elements, 31 | // This callback is called once the class is otherwise fully defined 32 | finisher(clazz) { 33 | window.customElements.define(tagName, clazz); 34 | } 35 | }; 36 | }; 37 | /** 38 | * Class decorator factory that defines the decorated class as a custom element. 39 | * 40 | * ``` 41 | * @customElement('my-element') 42 | * class MyElement { 43 | * render() { 44 | * return html``; 45 | * } 46 | * } 47 | * ``` 48 | * @category Decorator 49 | * @param tagName The name of the custom element to define. 50 | */ 51 | const customElement = (tagName) => (classOrDescriptor) => (typeof classOrDescriptor === 'function') ? 52 | legacyCustomElement(tagName, classOrDescriptor) : 53 | standardCustomElement(tagName, classOrDescriptor); 54 | const standardProperty = (options, element) => { 55 | // When decorating an accessor, pass it through and add property metadata. 56 | // Note, the `hasOwnProperty` check in `createProperty` ensures we don't 57 | // stomp over the user's accessor. 58 | if (element.kind === 'method' && element.descriptor && 59 | !('value' in element.descriptor)) { 60 | return Object.assign(Object.assign({}, element), { finisher(clazz) { 61 | clazz.createProperty(element.key, options); 62 | } }); 63 | } 64 | else { 65 | // createProperty() takes care of defining the property, but we still 66 | // must return some kind of descriptor, so return a descriptor for an 67 | // unused prototype field. The finisher calls createProperty(). 68 | return { 69 | kind: 'field', 70 | key: Symbol(), 71 | placement: 'own', 72 | descriptor: {}, 73 | // When @babel/plugin-proposal-decorators implements initializers, 74 | // do this instead of the initializer below. See: 75 | // https://github.com/babel/babel/issues/9260 extras: [ 76 | // { 77 | // kind: 'initializer', 78 | // placement: 'own', 79 | // initializer: descriptor.initializer, 80 | // } 81 | // ], 82 | initializer() { 83 | if (typeof element.initializer === 'function') { 84 | this[element.key] = element.initializer.call(this); 85 | } 86 | }, 87 | finisher(clazz) { 88 | clazz.createProperty(element.key, options); 89 | } 90 | }; 91 | } 92 | }; 93 | const legacyProperty = (options, proto, name) => { 94 | proto.constructor 95 | .createProperty(name, options); 96 | }; 97 | /** 98 | * A property decorator which creates a LitElement property which reflects a 99 | * corresponding attribute value. A [[`PropertyDeclaration`]] may optionally be 100 | * supplied to configure property features. 101 | * 102 | * This decorator should only be used for public fields. Private or protected 103 | * fields should use the [[`internalProperty`]] decorator. 104 | * 105 | * @example 106 | * ```ts 107 | * class MyElement { 108 | * @property({ type: Boolean }) 109 | * clicked = false; 110 | * } 111 | * ``` 112 | * @category Decorator 113 | * @ExportDecoratedItems 114 | */ 115 | function property(options) { 116 | // tslint:disable-next-line:no-any decorator 117 | return (protoOrDescriptor, name) => (name !== undefined) ? 118 | legacyProperty(options, protoOrDescriptor, name) : 119 | standardProperty(options, protoOrDescriptor); 120 | } 121 | 122 | export { customElement, property }; 123 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "snowpack dev", 4 | "build": "snowpack build", 5 | "test": "echo \"This template does not include a test runner by default.\" && exit 1", 6 | "format": "prettier --write \"src/**/*.js\"", 7 | "lint": "prettier --check \"src/**/*.js\"" 8 | }, 9 | "dependencies": { 10 | "lit-docs": "^0.0.33", 11 | "lit-element": "^2.3.1", 12 | "lit-html": "^1.2.1" 13 | }, 14 | "devDependencies": { 15 | "@babel/plugin-proposal-class-properties": "^7.10.4", 16 | "@babel/plugin-proposal-decorators": "^7.10.5", 17 | "@snowpack/plugin-babel": "^2.1.4", 18 | "@snowpack/plugin-dotenv": "^2.0.4", 19 | "prettier": "^2.0.5", 20 | "snowpack": "^2.17.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitaarik/lit-state/8cd66223612c3b115c0275f58f6cee5e900ee534/docs/public/favicon.png -------------------------------------------------------------------------------- /docs/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | LitState Docs 9 | 10 | 11 | 12 | 13 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/snowpack.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import("snowpack").SnowpackUserConfig } */ 2 | module.exports = { 3 | mount: { 4 | public: '/', 5 | src: '/dist', 6 | }, 7 | plugins: ['@snowpack/plugin-babel', '@snowpack/plugin-dotenv'], 8 | install: [ 9 | /* ... */ 10 | ], 11 | installOptions: { 12 | /* ... */ 13 | }, 14 | devOptions: { 15 | /* ... */ 16 | }, 17 | buildOptions: { 18 | // The build is meant for GitHub pages, so we need to set the path to the 19 | // GitHub pages URL as `baseUrl` 20 | baseUrl: '/lit-state/build/', 21 | metaDir: 'snowpack' 22 | }, 23 | proxy: { 24 | /* ... */ 25 | }, 26 | alias: { 27 | "@app": "./src" 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /docs/src/advanced-usage/manually-observe-state/index.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html } from 'lit-element'; 2 | import { LitDocsContent } from 'lit-docs'; 3 | import 'lit-docs'; 4 | import './observe-all-state-vars/automatic-component.js'; 5 | import './observe-all-state-vars/manual-component.js'; 6 | import './observe-specific-state-vars/specific-automatic-component.js'; 7 | import './observe-specific-state-vars/specific-manual-component.js'; 8 | 9 | 10 | @customElement('manually-observe-state') 11 | export class ManuallyObserveState extends LitDocsContent(LitElement) { 12 | 13 | render() { 14 | 15 | return html` 16 | 17 |

Manually observe the state

18 | 19 |

20 | When you use the observeState() mixin, 21 | your LitState Element automatically observes the states you use 22 | in that component, and re-renders when the state changes. You 23 | can also manually add more observers that will be notified when 24 | your state changes. This can be handy if you have other parts 25 | of your app (different from your LitElement components) that 26 | need to know when a state has changed. 27 |

28 | 29 |
30 | 31 | 32 |
33 | 34 |

35 | To add observers to a certain state instance, you just call 36 | myState.addObserver(myCallback). The 37 | callback will be called any time a 38 | stateVar changes. The callback will 39 | get as a first argument the name of the 40 | stateVar that changed. To stop 41 | observing, call 42 | myState.removeObserver(myCallback). It 43 | is similar to the way 44 | document.addEventListener() and 45 | document.removeEventListener() work. 46 |

47 | 48 |

49 | To keep the example simple, we still use a LitElement 50 | component. We just don't use the 51 | observeState() mixin. There are 52 | buttons to manually add and remove the observer: 53 | 54 |

55 | 56 |

57 | 58 |

Observe specific stateVars

59 | 60 |

61 | As an optional second argument to 62 | addObserver(), you can provide an 63 | array with stateVar names that you 64 | want to observe. The callback will then only be called when any 65 | of those stateVar variables change. 66 |

67 | 68 |
69 | 70 | 71 |
72 | 73 |

74 | 75 |

76 | 77 | `; 78 | 79 | } 80 | 81 | get manuallyObserveStateCode() { 82 | 83 | return `import { customElement, LitElement, property, html } from 'lit-element'; 84 | import { demoState } from './demo-state.js'; 85 | 86 | 87 | @customElement('manual-component') 88 | export class ManualComponent extends LitElement { 89 | 90 | @property({type: Boolean}) 91 | observing = false; 92 | 93 | render() { 94 | 95 | return html\` 96 | 97 |

<manual-component>

98 |

Counter: \${demoState.counter}

99 | 100 | 106 | 107 | 113 | 114 | \`; 115 | 116 | } 117 | 118 | handleObserveButtonClick() { 119 | this.stateObserver = () => this.requestUpdate(); 120 | demoState.addObserver(this.stateObserver); 121 | this.observing = true; 122 | } 123 | 124 | handleUnobserveButtonClick() { 125 | demoState.removeObserver(this.stateObserver); 126 | this.observing = false; 127 | } 128 | 129 | }`; 130 | 131 | } 132 | 133 | get manuallyObserveSpecificStateCode() { 134 | 135 | return `import { customElement, LitElement, property, html } from 'lit-element'; 136 | import { demoState } from './demo-state.js'; 137 | 138 | 139 | @customElement('specific-manual-component') 140 | export class SpecificManualComponent extends LitElement { 141 | 142 | @property({type: Boolean}) 143 | observingCounter1 = false; 144 | 145 | @property({type: Boolean}) 146 | observingCounter2 = false; 147 | 148 | render() { 149 | return html\` 150 | 151 |

<manual-component>

152 | 153 |

Counter1: \${demoState.counter1}

154 | 155 | 161 | 162 | 168 | 169 |

Counter2: \${demoState.counter2}

170 | 171 | 177 | 178 | 184 | 185 | \`; 186 | 187 | } 188 | 189 | handleObserveCounter1ButtonClick() { 190 | this.counter1Observer = () => this.requestUpdate(); 191 | demoState.addObserver(this.counter1Observer, ['counter1']); 192 | this.observingCounter1 = true; 193 | } 194 | 195 | handleUnobserveCounter1ButtonClick() { 196 | demoState.removeObserver(this.counter1Observer); 197 | this.observingCounter1 = false; 198 | } 199 | 200 | handleObserveCounter2ButtonClick() { 201 | this.counter2Observer = () => this.requestUpdate(); 202 | demoState.addObserver(this.counter2Observer, ['counter2']); 203 | this.observingCounter2 = true; 204 | } 205 | 206 | handleUnobserveCounter2ButtonClick() { 207 | demoState.removeObserver(this.counter2Observer); 208 | this.observingCounter2 = false; 209 | } 210 | 211 | }`; 212 | 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /docs/src/advanced-usage/manually-observe-state/observe-all-state-vars/automatic-component.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, html } from 'lit-element'; 2 | import { demoComponentStyle } from '@app/demo-component.js'; 3 | import { observeState } from '@app/lit-state.js'; 4 | import { demoState } from './state'; 5 | import 'lit-docs'; 6 | 7 | 8 | @customElement('automatic-component') 9 | export class AutomaticComponent extends observeState(demoComponentStyle(LitElement)) { 10 | 11 | render() { 12 | return html` 13 | 14 |

<automatic-component>

15 |

Counter: ${demoState.counter}

16 | 17 |
18 | `; 19 | } 20 | 21 | handleIncreaseCounterButtonClick() { 22 | demoState.counter++; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /docs/src/advanced-usage/manually-observe-state/observe-all-state-vars/manual-component.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html } from 'lit-element'; 2 | import { demoComponentStyle } from '@app/demo-component.js'; 3 | import { demoState } from './state'; 4 | import 'lit-docs'; 5 | 6 | 7 | @customElement('manual-component') 8 | export class ManualComponent extends demoComponentStyle(LitElement) { 9 | 10 | @property({type: Boolean}) 11 | observing = false; 12 | 13 | render() { 14 | 15 | return html` 16 | 17 | 18 | 19 |

<manual-component>

20 |

Counter: ${demoState.counter}

21 | 22 | 28 | 29 | 35 | 36 |
37 | 38 | `; 39 | 40 | } 41 | 42 | handleObserveButtonClick() { 43 | this.stateObserver = () => this.requestUpdate(); 44 | demoState.addObserver(this.stateObserver); 45 | this.observing = true; 46 | } 47 | 48 | handleUnobserveButtonClick() { 49 | demoState.removeObserver(this.stateObserver); 50 | this.observing = false; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /docs/src/advanced-usage/manually-observe-state/observe-all-state-vars/state.js: -------------------------------------------------------------------------------- 1 | import { LitState, stateVar } from '@app/lit-state.js'; 2 | 3 | 4 | class DemoState extends LitState { 5 | @stateVar() counter = 0; 6 | } 7 | 8 | 9 | export const demoState = new DemoState(); 10 | -------------------------------------------------------------------------------- /docs/src/advanced-usage/manually-observe-state/observe-specific-state-vars/specific-automatic-component.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, html } from 'lit-element'; 2 | import { demoComponentStyle } from '@app/demo-component.js'; 3 | import { observeState } from '@app/lit-state.js'; 4 | import { demoState } from './state'; 5 | import 'lit-docs'; 6 | 7 | 8 | @customElement('specific-automatic-component') 9 | export class SpecificAutomaticComponent extends observeState(demoComponentStyle(LitElement)) { 10 | 11 | render() { 12 | 13 | return html` 14 | 15 | 16 | 17 |

<automatic-component>

18 | 19 |

Counter1: ${demoState.counter1}

20 | 21 | 22 |

Counter2: ${demoState.counter2}

23 | 24 | 25 |
26 | 27 | `; 28 | 29 | } 30 | 31 | handleIncreaseCounter1ButtonClick() { 32 | demoState.counter1++; 33 | } 34 | 35 | handleIncreaseCounter2ButtonClick() { 36 | demoState.counter2++; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /docs/src/advanced-usage/manually-observe-state/observe-specific-state-vars/specific-manual-component.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html } from 'lit-element'; 2 | import { demoComponentStyle } from '@app/demo-component.js'; 3 | import { demoState } from './state'; 4 | import 'lit-docs'; 5 | 6 | 7 | @customElement('specific-manual-component') 8 | export class SpecificManualComponent extends demoComponentStyle(LitElement) { 9 | 10 | @property({type: Boolean}) 11 | observingCounter1 = false; 12 | 13 | @property({type: Boolean}) 14 | observingCounter2 = false; 15 | 16 | render() { 17 | 18 | return html` 19 | 20 | 21 | 22 |

<manual-component>

23 | 24 |

Counter1: ${demoState.counter1}

25 | 26 | 32 | 33 | 39 | 40 |

Counter2: ${demoState.counter2}

41 | 42 | 48 | 49 | 55 | 56 |
57 | 58 | `; 59 | 60 | } 61 | 62 | handleObserveCounter1ButtonClick() { 63 | this.counter1Observer = () => this.requestUpdate(); 64 | demoState.addObserver(this.counter1Observer, ['counter1']); 65 | this.observingCounter1 = true; 66 | } 67 | 68 | handleUnobserveCounter1ButtonClick() { 69 | demoState.removeObserver(this.counter1Observer); 70 | this.observingCounter1 = false; 71 | } 72 | 73 | handleObserveCounter2ButtonClick() { 74 | this.counter2Observer = () => this.requestUpdate(); 75 | demoState.addObserver(this.counter2Observer, ['counter2']); 76 | this.observingCounter2 = true; 77 | } 78 | 79 | handleUnobserveCounter2ButtonClick() { 80 | demoState.removeObserver(this.counter2Observer); 81 | this.observingCounter2 = false; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /docs/src/advanced-usage/manually-observe-state/observe-specific-state-vars/state.js: -------------------------------------------------------------------------------- 1 | import { LitState, stateVar } from '@app/lit-state.js'; 2 | 3 | 4 | class DemoState extends LitState { 5 | @stateVar() counter1 = 0; 6 | @stateVar() counter2 = 0; 7 | } 8 | 9 | 10 | export const demoState = new DemoState(); 11 | -------------------------------------------------------------------------------- /docs/src/advanced-usage/state-recorder-usage.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html } from 'lit-element'; 2 | import { LitDocsContent } from 'lit-docs'; 3 | import 'lit-docs'; 4 | 5 | 6 | @customElement('state-recorder-usage') 7 | export class StateRecorderUsage extends LitDocsContent(LitElement) { 8 | 9 | render() { 10 | 11 | return html` 12 | 13 |

stateRecorder

14 | 15 |

16 | LitState needs to know which stateVar variables 17 | are being used by your components. That's why you need to add 18 | the observeState() mixin to your 19 | LitElement components. The 20 | observeState() mixin uses the 21 | stateRecorder object to record which 22 | stateVar variables have been accessed during it's 23 | last render cycle. 24 |

25 | 26 |

Automatic usage with the observeState() mixin

27 | 28 |

29 | The observeState() mixin automatically uses the 30 | stateRecorder to record which 31 | stateVar variables are used by your component. 32 | Just before the component renders, 33 | stateRecorder.start() is being called to start 34 | recording. When a stateVar is being read, it's 35 | handler 36 | will record it to the stateRecorder. 37 |

38 | 39 |

40 | When the component has finished rendering, the 41 | stateRecorder.finish() method is called. This 42 | stops recording stateVar variables, and returns 43 | the recorded ones. These collected stateVar 44 | variables are then observed, and when one of them changes, the 45 | component re-renders itself. 46 |

47 | 48 |

49 | So when you use the observeState() mixin on your 50 | LitElement components, this is all being taken 51 | care of for you. 52 |

53 | 54 |

Manual usage

55 | 56 |

57 | For custom implementations, you might want to use the 58 | stateRecorder manually. You could for example, 59 | create a observeState() variation for React. That 60 | would re-render a React component when a stateVar 61 | that is used inside the component changes. See the 62 | API reference 63 | for more information on how to use stateRecorder. 64 |

65 | 66 | `; 67 | 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /docs/src/advanced-usage/state-var-handler/custom-state-var-handler-component.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, html } from 'lit-element'; 2 | import { demoComponentStyle } from '@app/demo-component.js'; 3 | import { observeState } from '@app/lit-state.js'; 4 | import { demoState } from './state'; 5 | import 'lit-docs'; 6 | 7 | 8 | @customElement('custom-state-var-handler-component') 9 | export class CustomStateVarHandlerComponent extends observeState(demoComponentStyle(LitElement)) { 10 | 11 | render() { 12 | 13 | return html` 14 | 15 | 16 | 17 |

<custom-state-var-handler-component>

18 |

19 | 20 | ${demoState.counter} 21 | 22 |

23 | 24 |
25 | 26 | `; 27 | 28 | } 29 | 30 | handleDecreaseCounterButtonClick() { 31 | demoState.counter--; 32 | } 33 | 34 | handleIncreaseCounterButtonClick() { 35 | demoState.counter++; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /docs/src/advanced-usage/state-var-handler/index.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html } from 'lit-element'; 2 | import { LitDocsContent } from 'lit-docs'; 3 | import 'lit-docs'; 4 | import './custom-state-var-handler-component'; 5 | 6 | 7 | @customElement('state-var-handler') 8 | export class StateVarHandler extends LitDocsContent(LitElement) { 9 | 10 | render() { 11 | 12 | return html` 13 | 14 |

stateVar handler

15 | 16 |

17 | You can also define your own stateVar handler 18 | class. This allows you to have custom functionality when you 19 | read/write your stateVars. An example of a custom state var is 20 | asyncStateVar. 21 | This is a type of stateVar that makes it easy to deal with 22 | asynchronous data, and the state of the data (loading, 23 | updating, loaded, updated, etc). 24 |

25 | 26 |

27 | On this page we'll make a custom stateVar handler ourselves. 28 | We'll create a LocalStorageHandler that saves your 29 | stateVar values to localStorage. So 30 | that your state is retained when you refresh your page. 31 |

32 | 33 |

34 | Here is the component already. Later we'll explain how it is 35 | made. To try it out, change the value and refresh the page. 36 |

37 | 38 |
39 | 40 |
41 | 42 |

Default StateVar handler

43 | 44 |

45 | We implement this handler by creating a new class that extends 46 | from the default handler class 47 | StateVar. This default handler class 48 | looks like this: 49 |

50 | 51 |

52 | 53 |

54 | 55 |

Custom StateVar handler

56 | 57 |

58 | We will extend this class and add some functionality. You see 59 | that the constructor of the default handler sets some default 60 | variables. We will use some of these in our custom handler. 61 |

62 | 63 |

64 | 65 |

66 | 67 |

68 | Like this, when the stateVar is 69 | created, the initial value is set to any previously set 70 | localStorage value, or else to 71 | options.initialValue. And whenever a 72 | new value is set, it is saved to 73 | localStorage. The option 74 | localStorageKey is used as the key for 75 | the localStorage. 76 |

77 | 78 |

Usage in state class

79 | 80 |

81 | Now let's see how we use this custom 82 | stateVar handler in our 83 | demoState class: 84 |

85 | 86 |

87 | 88 |

89 | 90 |

91 | You see that we tell LitState to use a different handler by 92 | specifying the handler option. The 93 | other options are options of our own custom handler. The 94 | initialValue should be set through an 95 | option. It can't be set like counter = 0, 96 | because that would be seen as a regular assignment, and would 97 | override any previously value in 98 | localStorage. 99 |

100 | 101 |

Custom decorator

102 | 103 |

104 | If you use your custom stateVar 105 | handler a lot, it could be useful to also make a custom 106 | decorator function: 107 |

108 | 109 |

110 | 111 |

112 | 113 |

114 | This allows you to define the stateVar 115 | like this: 116 |

117 | 118 |

119 | 120 |

121 | 122 |

123 | Custom stateVar handlers give you a 124 | lot of power for customizing what happens when your 125 | stateVar variables are being get/set. 126 |

127 | 128 |

No-decorator usage

129 | 130 |

131 | Without decorators, use the static stateVarOptions() 132 | getter method to specify the options: 133 |

134 | 135 |

136 | 137 |

138 | 139 |

Providing options from a method

140 | 141 |

142 | To give you access to the this objects on your 143 | state instance, you can additionally add options to your 144 | handler through a method. 145 |

146 | 147 |

148 | 149 |

150 | 151 | `; 152 | 153 | } 154 | 155 | get litStateStateVarHandlerCode() { 156 | 157 | return `// ... 158 | 159 | export class StateVar { 160 | 161 | constructor(args) { 162 | this.options = args.options; // The options given in the \`stateVar\` declaration 163 | this.recordRead = args.recordRead; // Callback to indicate the \`stateVar\` is read 164 | this.notifyChange = args.notifyChange; // Callback to indicate the \`stateVar\` value has changed 165 | this.value = undefined; // The initial value 166 | } 167 | 168 | // Called when the \`stateVar\` on the \`State\` class is read. 169 | get() { 170 | this.recordRead(); 171 | return this.value; 172 | } 173 | 174 | // Returns whether the given \`value\` should be passed on to the \`set()\` 175 | // method. Can be used for validation and/or optimization. 176 | shouldSetValue(value) { 177 | return this.value !== value; 178 | } 179 | 180 | // Called when the \`stateVar\` on the \`State\` class is set. 181 | set(value) { 182 | this.value = value; 183 | this.notifyChange() 184 | } 185 | 186 | } 187 | 188 | // ...`; 189 | 190 | } 191 | 192 | get localStorageHandlerCode() { 193 | 194 | return `import { StateVar } from 'lit-element-state'; 195 | 196 | export class LocalStorageHandler extends StateVar { 197 | 198 | constructor(args) { 199 | super(args); 200 | this.value = ( 201 | localStorage.getItem(this.options.localStorageKey) 202 | || this.options.initialValue 203 | ); 204 | } 205 | 206 | set(value) { 207 | super.set(value); 208 | localStorage.setItem(this.options.localStorageKey, value); 209 | } 210 | 211 | }`; 212 | 213 | } 214 | 215 | get demoStateCode() { 216 | 217 | return `import { LitState, stateVar } from 'lit-element-state'; 218 | 219 | class DemoState extends LitState { 220 | 221 | @stateVar({ 222 | handler: LocalStorageHandler, 223 | localStorageKey: 'counter', 224 | initialValue: 0 225 | }) 226 | counter; 227 | 228 | }`; 229 | 230 | } 231 | 232 | get localStorageHandlerDecoratorCode() { 233 | 234 | return `function localStorageStateVar(options) { 235 | return stateVar(Object.assign( 236 | {handler: LocalStorageHandler}, 237 | options 238 | )); 239 | }`; 240 | 241 | } 242 | 243 | get customDecoratorCode() { 244 | 245 | return `class DemoState extends LitState { 246 | 247 | @localStorageStateVar({ 248 | localStorageKey: 'counter', 249 | initialValue: 0 250 | }) 251 | counter; 252 | 253 | }`; 254 | 255 | } 256 | 257 | get customStatevarNoDecoratorUsageCode() { 258 | 259 | return`function localStorageStateVar(options) { 260 | return Object.assign({ 261 | {handler: LocalStorageHandler}, 262 | options 263 | }); 264 | } 265 | 266 | class DemoState extends LitState { 267 | 268 | static get stateVarOptions() { 269 | return { 270 | myVar: localStorageStateVar({ 271 | localStorageKey: 'counter', 272 | initialValue: 0 273 | }) 274 | } 275 | } 276 | 277 | }`; 278 | 279 | } 280 | 281 | get methodDecoratingCode() { 282 | 283 | return `class DemoState extends LitState { 284 | 285 | @stateVar({ handler: MyCustomHandler }) 286 | myVar() { 287 | // This object returned, will be added to the \`options\` that are 288 | // given to the constructor of the \`handler\` class. 289 | return { 290 | callback: () => this.callback(); 291 | }; 292 | } 293 | 294 | }`; 295 | 296 | } 297 | 298 | } 299 | -------------------------------------------------------------------------------- /docs/src/advanced-usage/state-var-handler/state.js: -------------------------------------------------------------------------------- 1 | import { LitState, stateVar, StateVar } from '@app/lit-state.js'; 2 | 3 | 4 | class LocalStorageHandler extends StateVar { 5 | 6 | constructor(args) { 7 | super(args); 8 | this.value = ( 9 | localStorage.getItem(this.options.localStorageKey) 10 | || this.options.initialValue 11 | ); 12 | } 13 | 14 | set(value) { 15 | super.set(value); 16 | localStorage.setItem(this.options.localStorageKey, value); 17 | } 18 | 19 | } 20 | 21 | 22 | function localStorageStateVar(options) { 23 | return stateVar(Object.assign( 24 | {handler: LocalStorageHandler}, 25 | options 26 | )); 27 | } 28 | 29 | 30 | class DemoState extends LitState { 31 | 32 | @localStorageStateVar({ 33 | localStorageKey: 'counter', 34 | initialValue: 0 35 | }) 36 | counter; 37 | 38 | } 39 | 40 | 41 | export const demoState = new DemoState(); 42 | -------------------------------------------------------------------------------- /docs/src/api/lit-state.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html } from 'lit-element'; 2 | import { LitDocsContent } from 'lit-docs'; 3 | import 'lit-docs'; 4 | 5 | 6 | @customElement('api-lit-state') 7 | export class ApiLitState extends LitDocsContent(LitElement) { 8 | 9 | render() { 10 | 11 | return html` 12 | 13 |

LitState

14 | 15 |

16 | This is the API reference for advanced usage. See 17 | Basic usage 18 | on how to use this class. 19 |

20 | 21 |

class LitState

22 | 23 |

24 | Property stateVars 25 |

26 | 27 |

28 | Contains the stateVar variables that have been 29 | defined (either through decorators or not). It's an object with 30 | the name in the key and the options in the value: 31 |

32 | 33 |

34 | 35 |

36 | 37 |

38 | The default stateVar handler doesn't require any 39 | options. But the options can be used to specify a 40 | custom 41 | stateVar handler. 42 | A custom handler might also accept extra options: 43 |

44 | 45 |

46 | 47 |

48 | 49 |

Method addObserver(observer, keys)

50 | 51 |

52 | Add a observer to this state class. This is for when you need 53 | to observe the state outside of a LitElement component, see 54 | Manually observe state. 55 | For usage with LitElement, use the 56 | observeState() mixin. 57 |

58 | 59 |

observer

60 | 61 |

62 | Function that will be called when the state changes. This 63 | function will get as a first argument the name of the 64 | stateVar that has been changed. 65 |

66 | 67 |

keys

68 | 69 |

70 | An Array of names of stateVar variables to 71 | observe. Only if those stateVar variables change, 72 | the observer will be called. If not provided, the 73 | observer will be called when any 74 | stateVar variable changes. 75 |

76 | 77 |

78 | 79 |

80 | 81 |

Method removeObserver(observer)

82 | 83 |

84 | Removes a previously added observer. The observer 85 | argument should be the same as the observer 86 | argument given to addObserver(): 87 |

88 | 89 |

90 | 91 |

92 | 93 | `; 94 | 95 | } 96 | 97 | get stateVarFormat() { 98 | return `{ 99 | myStateVar1: {}, 100 | myStateVar2: {} 101 | }`; 102 | } 103 | 104 | get stateVarFormatWithCustomHandler() { 105 | return `{ 106 | myStateVar1: {}, 107 | myStateVar2: {}, 108 | customStateVar: { 109 | handler: CustomStateVarHandler, 110 | extraOption: 'value' 111 | } 112 | }`; 113 | } 114 | 115 | get addObserverCode() { 116 | return `const myObserver = function(name) { 117 | console.log('stateVar', name, 'changed!'); 118 | } 119 | 120 | // Observe any \`stateVar\` variable: 121 | myState.addObserver(myObserver); 122 | 123 | // Or only observe specific \`stateVar\` variables: 124 | myState.addObserver(myObserver, ['myStateVar1', 'myStateVar2']);`; 125 | } 126 | 127 | get removeObserverCode() { 128 | return `const myObserver = function(changedKey) { /* ... */ } 129 | 130 | // Add the observer: 131 | myState.addObserver(myObserver); 132 | 133 | // ... later on remove the observer again: 134 | myState.removeObserver(myObserver);`; 135 | } 136 | 137 | 138 | } 139 | -------------------------------------------------------------------------------- /docs/src/api/observe-state-mixin.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html, css } from 'lit-element'; 2 | import { LitDocsContent } from 'lit-docs'; 3 | import 'lit-docs'; 4 | 5 | 6 | @customElement('api-observe-state-mixin') 7 | export class ApiObserveStateMixin extends LitDocsContent(LitElement) { 8 | 9 | render() { 10 | 11 | return html` 12 | 13 |

observeState() mixin

14 | 15 |

16 | This mixin is meant to be used on your components that extend 17 | from LitElement. You apply the mixin by wrapping 18 | the LitElement class with this mixin function: 19 | observeState(LitElement). 20 |

21 | 22 |

23 | This makes your LitElement component aware of any 24 | stateVar variables. When stateVar 25 | variables are being read by the component during render, it 26 | will record this. Then when any of the recorded 27 | stateVar variables are being changed, the 28 | component re-renders itself. 29 |

30 | 31 |

observeState(litElementClass)

32 | 33 |

34 | Add "stateVar awareness" to the given 35 | litElementClass, which should be a class 36 | definition of, or an extend from, LitElement's 37 | LitElement class. 38 |

39 | 40 | `; 41 | 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /docs/src/api/state-recorder.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html } from 'lit-element'; 2 | import { LitDocsContent } from 'lit-docs'; 3 | import 'lit-docs'; 4 | 5 | 6 | @customElement('api-state-recorder') 7 | export class ApiStateRecorder extends LitDocsContent(LitElement) { 8 | 9 | render() { 10 | 11 | return html` 12 | 13 |

stateRecorder object

14 | 15 |

16 | This object is used by the 17 | observeState() mixin, 18 | to get the stateVar variabes that have been 19 | accesed during a render cycle. 20 |

21 | 22 |

Methods

23 | 24 |

start()

25 | 26 |

27 | Starts the recorder. After calling this, every 28 | stateVar variable that is being read, will be 29 | recorded, until finish() is called. 30 |

31 | 32 |

recordRead(stateObj, key)

33 | 34 |

35 | If there is a recorder started (by start()) and 36 | hasn't finished yet (by finish()); records that a 37 | stateVar has been read. 38 |

39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 56 |
stateObj 44 | The instance of the LitState derived 45 | class, on which the accessed stateVar has 46 | been defined. 47 | 48 |
key 52 | The varname of the stateVar variable as 53 | defined on the LitState class. 54 | 55 |
57 | 58 |

finish()

59 | 60 |

61 | Stops recording stateVar variables, started by the 62 | start() method, and returns the recorded 63 | stateVar variables in a Map() object. 64 |

65 | 66 | `; 67 | 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /docs/src/api/state-var-decorator.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html } from 'lit-element'; 2 | import { LitDocsContent } from 'lit-docs'; 3 | import 'lit-docs'; 4 | 5 | 6 | @customElement('api-state-var-decorator') 7 | export class ApiStateVarDecorator extends LitDocsContent(LitElement) { 8 | 9 | render() { 10 | 11 | return html` 12 | 13 |

@stateVar() decorator

14 | 15 |

16 | This decorator function is used to define stateVar 17 | variables on a LitState derived class. When these 18 | variables are set or get from the LitState class, 19 | for example myState.myStateVar = 'value', the 20 | observers of the LitState class will be notified. 21 |

22 | 23 |

@stateVar(options)

24 | 25 |

options

26 | 27 |

28 | Optional parameter that should be a object containing the 29 | options for the StateVar handler class. The 30 | default StateVar handler class doesn't take any 31 | options. You can use the option handler to specify 32 | a custom 33 | StateVar handler class. Other 34 | options you specify will be passed on to this custom 35 | StateVar handler class. 36 |

37 | 38 |

39 | 40 |

41 | 42 | `; 43 | 44 | } 45 | 46 | get stateVarOptionsCode() { 47 | 48 | return `@stateVar({ 49 | handler: MyStateVarHandler, 50 | myOption: 'value' 51 | }) 52 | myStateVar = 'value';`; 53 | 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /docs/src/api/state-var-handler.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html, css } from 'lit-element'; 2 | import { LitDocsContent } from 'lit-docs'; 3 | import 'lit-docs'; 4 | 5 | 6 | @customElement('api-state-var-handler') 7 | export class ApiStateVarHandler extends LitDocsContent(LitElement) { 8 | 9 | render() { 10 | 11 | return html` 12 | 13 |

StateVar handler

14 | 15 |

16 | This is the default handler class for the stateVar 17 | variables. When you define a stateVar and you 18 | don't specify a custom handler class, this class will be used. 19 |

20 | 21 |

22 | A handler class controls what happens when a 23 | stateVar is being set or get. For more information 24 | on how to create a custom stateVar handler class, 25 | see stateVar handler. 26 |

27 | 28 |

29 | You shouldn't use a StateVar handler class 30 | directly as a user. You only need to know about it when 31 | defining a custom StateVar handler class. The 32 | handler class is called by LitState internally, so you don't 33 | need to call it yourself. 34 |

35 | 36 |

class StateVar

37 | 38 |

Methods

39 | 40 |

constructor(args)

41 | 42 |

43 | The constructor takes a single argument args which 44 | is an object containing the following properties: 45 |

46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
optionsThe options given in the stateVar declaration.
recordReadCallback to indicate the stateVar is read.
notifyChangeCallback to indicate the stateVar value has changed.
valueThe initial value.
65 | 66 |

get()

67 | 68 |

69 | Called when the stateVar on the 70 | LitState class is read (for example: 71 | myState.myStateVar). Should return the value of 72 | the stateVar. Typically, this method should call 73 | the recordRead() callback, set in the 74 | constructor(), to indicate the observers that the 75 | stateVar is being read. 76 |

77 | 78 |

shouldSetValue(value)

79 | 80 |

81 | Called before the set() method is called. If this 82 | method returns false, the set() 83 | method won't be called. This can be used for validation and/or 84 | optimization. 85 |

86 | 87 |

set(value)

88 | 89 |

90 | Called when the stateVar on the 91 | LitState class is set (for example: 92 | myState.myStateVar = 'value'. 93 |

94 | 95 |

96 | Should set the value of the stateVar. Typically, 97 | this method would call the notifyChange() 98 | callback, set in the constructor(), to indicate 99 | the observers that the stateVar has changed. 100 |

101 | 102 | `; 103 | 104 | } 105 | 106 | get defaultStateVarHandlerGet() { 107 | return `get() { 108 | this.recordRead(); 109 | return this.value; 110 | }`; 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /docs/src/basic-usage/index.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html } from 'lit-element'; 2 | import { LitDocsContent } from 'lit-docs'; 3 | import 'lit-docs'; 4 | import './state-var-component-1'; 5 | import './state-var-component-2'; 6 | 7 | 8 | @customElement('basic-usage') 9 | export class BasicUsage extends LitDocsContent(LitElement) { 10 | 11 | render() { 12 | 13 | return html` 14 | 15 |

Basic usage

16 | 17 |

18 | Below are 2 components with a shared state 19 | demoState. When you change the state 20 | from one component, the other component automatically 21 | synchronizes: 22 |

23 | 24 |
25 | 26 | 27 |
28 | 29 |

The state class

30 | 31 |

32 | The shared state demoState contains a 33 | stateVar called 34 | counter. It holds an integer that has 35 | an initial value of 0: 36 |

37 | 38 |

39 | 40 |

41 | 42 |

43 | 44 | Look here 45 | for usage without 46 | decorators. 47 | 48 |

49 | 50 |

Usage in component

51 | 52 |

53 | The components that use the state use the mixin 54 | observeState. This makes them 55 | automatically re-render when a 56 | stateVar they use changes: 57 |

58 | 59 |

60 | 61 |

62 | 63 |

64 | That's all. How simple do you want to have it? 65 |

66 | 67 | `; 68 | 69 | } 70 | 71 | get demoStateCode() { 72 | 73 | return `import { LitState, stateVar } from 'lit-element-state'; 74 | 75 | class DemoState extends LitState { 76 | @stateVar() counter = 0; 77 | } 78 | 79 | export const demoState = new DemoState();`; 80 | 81 | } 82 | 83 | get componentCode() { 84 | 85 | return `import { customElement, LitElement, html } from 'lit-element'; 86 | import { observeState } from 'lit-element-state'; 87 | import { demoState } from './demo-state.js'; 88 | 89 | @customElement('component-1') 90 | export class Component1 extends observeState(LitElement) { 91 | 92 | render() { 93 | return html\` 94 |

<component-1>

95 |

Counter: \${demoState.counter}

96 | 99 | \`; 100 | } 101 | 102 | handleIncreaseCounterButtonClick() { 103 | demoState.counter++; 104 | } 105 | 106 | }`; 107 | 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /docs/src/basic-usage/no-decorator-usage/index.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html } from 'lit-element'; 2 | import { LitDocsContent } from 'lit-docs'; 3 | import 'lit-docs'; 4 | import './no-deco-component-1'; 5 | import './no-deco-component-2'; 6 | 7 | 8 | @customElement('no-decorator-usage') 9 | export class NoDecoratorUsage extends LitDocsContent(LitElement) { 10 | 11 | render() { 12 | 13 | return html` 14 | 15 |

Usage without decorators

16 | 17 |

18 | In case you can't or don't want to use 19 | decorators, 20 | you can define the stateVars with a static 21 | stateVars getter method. This method should return 22 | an object with in the keys the name of the 23 | stateVar and in the value the initial value for 24 | it. 25 |

26 | 27 |

Example

28 | 29 |

30 | 31 |

32 | 33 |

Output

34 | 35 |
36 | 37 | 38 |
39 | 40 | `; 41 | 42 | } 43 | 44 | get demoStateCode() { 45 | 46 | return `import { LitState } from 'lit-element-state'; 47 | 48 | class DemoState extends LitState { 49 | 50 | static get stateVars() { 51 | return { 52 | counter: 0 53 | }; 54 | } 55 | 56 | } 57 | 58 | export const demoState = new DemoState();`; 59 | 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /docs/src/basic-usage/no-decorator-usage/no-deco-component-1.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, html } from 'lit-element'; 2 | import { demoComponentStyle } from '@app/demo-component.js'; 3 | import { observeState } from '@app/lit-state.js'; 4 | import { demoState } from './state'; 5 | import 'lit-docs'; 6 | 7 | 8 | @customElement('no-deco-component-1') 9 | export class NoDecoComponent1 extends observeState(demoComponentStyle(LitElement)) { 10 | 11 | render() { 12 | return html` 13 | 14 |

<component-1>

15 |

Counter: ${demoState.counter}

16 | 17 |
18 | `; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /docs/src/basic-usage/no-decorator-usage/no-deco-component-2.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, html } from 'lit-element'; 2 | import { demoComponentStyle } from '@app/demo-component.js'; 3 | import { observeState } from '@app/lit-state.js'; 4 | import { demoState } from './state'; 5 | import 'lit-docs'; 6 | 7 | 8 | @customElement('no-deco-component-2') 9 | export class NoDecoComponent2 extends observeState(demoComponentStyle(LitElement)) { 10 | 11 | render() { 12 | return html` 13 | 14 |

<component-2>

15 |

Counter: ${demoState.counter}

16 | 17 |
18 | `; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /docs/src/basic-usage/no-decorator-usage/state.js: -------------------------------------------------------------------------------- 1 | import { LitState } from '@app/lit-state.js'; 2 | 3 | 4 | class DemoState extends LitState { 5 | 6 | static get stateVars() { 7 | return { 8 | counter: 0 9 | }; 10 | } 11 | 12 | } 13 | 14 | 15 | export const demoState = new DemoState(); 16 | -------------------------------------------------------------------------------- /docs/src/basic-usage/state-var-component-1.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, html } from 'lit-element'; 2 | import { demoComponentStyle } from '@app/demo-component.js'; 3 | import { observeState } from '@app/lit-state.js'; 4 | import { demoState } from './state'; 5 | import 'lit-docs'; 6 | 7 | 8 | @customElement('state-var-component-1') 9 | export class StateVarComponent1 extends observeState(demoComponentStyle(LitElement)) { 10 | 11 | render() { 12 | return html` 13 | 14 |

<component-1>

15 |

Counter: ${demoState.counter}

16 | 17 |
18 | `; 19 | } 20 | 21 | handleIncreaseCounterButtonClick() { 22 | demoState.counter++; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /docs/src/basic-usage/state-var-component-2.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, html } from 'lit-element'; 2 | import { demoComponentStyle } from '@app/demo-component.js'; 3 | import { observeState } from '@app/lit-state.js'; 4 | import { demoState } from './state'; 5 | import 'lit-docs'; 6 | 7 | 8 | @customElement('state-var-component-2') 9 | export class StateVarComponent1 extends observeState(demoComponentStyle(LitElement)) { 10 | 11 | render() { 12 | return html` 13 | 14 |

<component-2>

15 |

Counter: ${demoState.counter}

16 | 17 |
18 | `; 19 | } 20 | 21 | handleIncreaseCounterButtonClick() { 22 | demoState.counter++; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/src/basic-usage/state.js: -------------------------------------------------------------------------------- 1 | import { LitState, stateVar } from '@app/lit-state.js'; 2 | 3 | 4 | class DemoState extends LitState { 5 | @stateVar() counter = 0; 6 | } 7 | 8 | 9 | export const demoState = new DemoState(); 10 | -------------------------------------------------------------------------------- /docs/src/demo-component.js: -------------------------------------------------------------------------------- 1 | import { css } from 'lit-element'; 2 | import { litStyle } from 'lit-element-style'; 3 | import 'lit-docs'; 4 | 5 | 6 | export const demoComponentStyle = litStyle(css` 7 | 8 | h2 { 9 | margin-top: 0; 10 | font-size: 20px; 11 | color: green; 12 | } 13 | 14 | h3 { 15 | margin: 20px 0; 16 | font-weight: 600; 17 | font-size: 16px; 18 | } 19 | 20 | .status { 21 | color: blue; 22 | } 23 | 24 | .value { 25 | color: #cb2828; 26 | } 27 | 28 | .buttons { 29 | display: flex; 30 | flex-wrap: wrap; 31 | margin: -5px 0 0 -5px; 32 | } 33 | 34 | .buttons > * { 35 | margin: 5px 0 0 5px; 36 | } 37 | 38 | `); 39 | -------------------------------------------------------------------------------- /docs/src/index.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html } from 'lit-element'; 2 | import 'lit-docs'; 3 | import './intro-page'; 4 | import './basic-usage/index'; 5 | import './basic-usage/no-decorator-usage/index'; 6 | import './state-handling/index'; 7 | import './state-handling/computed-values/index'; 8 | import './state-handling/nested-states/index'; 9 | import './advanced-usage/state-var-handler/index'; 10 | import './advanced-usage/manually-observe-state/index'; 11 | import './advanced-usage/state-recorder-usage'; 12 | import './api/lit-state'; 13 | import './api/state-var-handler'; 14 | import './api/observe-state-mixin'; 15 | import './api/state-var-decorator'; 16 | import './api/state-recorder'; 17 | import './use-cases/index'; 18 | import './use-cases/different-vars-on-rerender/index'; 19 | import './use-cases/reconnected-components/index'; 20 | 21 | 22 | @customElement('lit-state-docs') 23 | export class LitStateDocs extends LitElement { 24 | 25 | render() { 26 | return html``; 27 | } 28 | 29 | get pages() { 30 | return [ 31 | { 32 | title: 'Introduction', 33 | path: 'intro-page', 34 | template: html`` 35 | }, 36 | { 37 | title: 'Basic usage', 38 | path: 'basic-usage', 39 | template: html``, 40 | submenu: [ 41 | { 42 | title: 'Usage without decorators', 43 | path: 'no-decorator-usage', 44 | template: html`` 45 | } 46 | ] 47 | }, 48 | { 49 | title: 'State handling', 50 | path: 'state-handling', 51 | template: html``, 52 | submenu: [ 53 | { 54 | title: 'Computed values', 55 | path: 'computed-values', 56 | template: html`` 57 | }, 58 | { 59 | title: 'Nested states', 60 | path: 'nested-states', 61 | template: html`` 62 | } 63 | ] 64 | }, 65 | { 66 | title: 'Advanced usage', 67 | path: 'advanced-usage', 68 | submenu: [ 69 | { 70 | title: 'StateVar handler', 71 | path: 'state-var-handler', 72 | template: html`` 73 | }, 74 | { 75 | title: 'Manually observe state', 76 | path: 'manually-observe-state', 77 | template: html`` 78 | }, 79 | { 80 | title: 'stateRecorder', 81 | path: 'state-recorder-usage', 82 | template: html`` 83 | } 84 | ] 85 | }, 86 | { 87 | title: 'API reference', 88 | path: 'api-reference', 89 | submenu: [ 90 | { 91 | title: 'LitState class', 92 | path: 'lit-state', 93 | template: html`` 94 | }, 95 | { 96 | title: '@stateVar() decorator', 97 | path: 'state-var-decorator', 98 | template: html`` 99 | }, 100 | { 101 | title: 'observeState() mixin', 102 | path: 'observe-state-mixin', 103 | template: html`` 104 | }, 105 | { 106 | title: 'StateVar handler class', 107 | path: 'state-var-handler', 108 | template: html`` 109 | }, 110 | { 111 | title: 'stateRecorder object', 112 | path: 'state-recorder', 113 | template: html`` 114 | } 115 | ] 116 | }, 117 | { 118 | title: 'Use case coverage', 119 | path: 'test-cases', 120 | template: html``, 121 | submenu: [ 122 | { 123 | title: 'Different vars on re-render', 124 | path: 'different-vars-on-rerender', 125 | template: html`` 126 | }, 127 | { 128 | title: 'Reconnected components', 129 | path: 'reconnected-components', 130 | template: html`` 131 | } 132 | ] 133 | } 134 | ]; 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /docs/src/intro-page.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html } from 'lit-element'; 2 | import { LitDocsContent } from 'lit-docs'; 3 | import 'lit-docs'; 4 | 5 | 6 | @customElement('intro-page') 7 | export class IntroPage extends LitDocsContent(LitElement) { 8 | 9 | render() { 10 | 11 | return html` 12 | 13 |

LitState

14 | 15 |

Simple shared app state management for LitElement

16 | 17 |

18 | LitState automatically re-renders your LitElement 19 | components, when a shared app state variable they use 20 | changes. It's like LitElement's 21 | properties, 22 | but then shared over multiple components. 23 |

24 | 25 |

Installation

26 | 27 |

28 | 29 |

30 | 31 |

Usage

32 | 33 |

1. Create a LitState object:

34 | 35 |

36 | Use the stateVar() decorator to define 37 | the variables that should be observed by LitState. 38 |

39 | 40 |

41 | 42 |

43 | 44 |

2. Make your component aware of your state:

45 | 46 |

47 | Use the observeState() mixin on your 48 | LitElement components to make them 49 | re-render when any stateVar variables 50 | they use changes. 51 |

52 | 53 |

54 | 55 |

56 | 57 | `; 58 | 59 | } 60 | 61 | get stateCode() { 62 | 63 | return `import { LitState, stateVar } from 'lit-element-state'; 64 | 65 | class MyState extends LitState { 66 | @stateVar() counter = 0; 67 | } 68 | 69 | export const myState = new MyState();`; 70 | 71 | } 72 | 73 | get componentCode() { 74 | 75 | return `import { LitElement, html } from 'lit-element'; 76 | import { observeState } from 'lit-element-state'; 77 | import { myState } form './my-state.js'; 78 | 79 | class MyComponent extends observeState(LitElement) { 80 | 81 | render() { 82 | return html\` 83 |

Counter: \${myState.counter}

84 | 85 | \`; 86 | } 87 | 88 | handleIncreaseCounterButtonClick() { 89 | myState.counter++; 90 | } 91 | 92 | }`; 93 | 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /docs/src/lit-state.js: -------------------------------------------------------------------------------- 1 | export const observeState = superclass => class extends superclass { 2 | 3 | constructor() { 4 | super(); 5 | this._observers = []; 6 | } 7 | 8 | update(changedProperties) { 9 | stateRecorder.start(); 10 | super.update(changedProperties); 11 | this._initStateObservers(); 12 | } 13 | 14 | connectedCallback() { 15 | super.connectedCallback(); 16 | if (this._wasConnected) { 17 | this.requestUpdate(); 18 | delete this._wasConnected; 19 | } 20 | } 21 | 22 | disconnectedCallback() { 23 | super.disconnectedCallback(); 24 | this._wasConnected = true; 25 | this._clearStateObservers(); 26 | } 27 | 28 | _initStateObservers() { 29 | this._clearStateObservers(); 30 | if (!this.isConnected) return; 31 | this._addStateObservers(stateRecorder.finish()); 32 | } 33 | 34 | _addStateObservers(stateVars) { 35 | for (let [state, keys] of stateVars) { 36 | const observer = () => this.requestUpdate(); 37 | this._observers.push([state, observer]); 38 | state.addObserver(observer, keys); 39 | } 40 | } 41 | 42 | _clearStateObservers() { 43 | for (let [state, observer] of this._observers) { 44 | state.removeObserver(observer); 45 | } 46 | this._observers = []; 47 | } 48 | 49 | } 50 | 51 | 52 | export class LitState { 53 | 54 | constructor() { 55 | this._observers = []; 56 | this._initStateVars(); 57 | } 58 | 59 | addObserver(observer, keys) { 60 | this._observers.push({observer, keys}); 61 | } 62 | 63 | removeObserver(observer) { 64 | this._observers = this._observers.filter(observerObj => observerObj.observer !== observer); 65 | } 66 | 67 | _initStateVars() { 68 | 69 | if (this.constructor.stateVarOptions) { 70 | for (let [key, options] of Object.entries(this.constructor.stateVarOptions)) { 71 | this._initStateVar(key, options); 72 | } 73 | } 74 | 75 | if (this.constructor.stateVars) { 76 | for (let [key, value] of Object.entries(this.constructor.stateVars)) { 77 | this._initStateVar(key, {}); 78 | this[key] = value; 79 | } 80 | } 81 | 82 | } 83 | 84 | _initStateVar(key, options) { 85 | 86 | if (this.hasOwnProperty(key)) { 87 | // Property already defined, so don't re-define. 88 | return; 89 | } 90 | 91 | options = this._parseOptions(options); 92 | 93 | const stateVar = new options.handler({ 94 | options: options, 95 | recordRead: () => this._recordRead(key), 96 | notifyChange: () => this._notifyChange(key) 97 | }); 98 | 99 | Object.defineProperty( 100 | this, 101 | key, 102 | { 103 | get() { 104 | return stateVar.get(); 105 | }, 106 | set(value) { 107 | if (stateVar.shouldSetValue(value)) { 108 | stateVar.set(value); 109 | } 110 | }, 111 | configurable: true, 112 | enumerable: true 113 | } 114 | ); 115 | 116 | } 117 | 118 | _parseOptions(options) { 119 | 120 | if (!options.handler) { 121 | options.handler = StateVar; 122 | } else { 123 | 124 | // In case of a custom `StateVar` handler is provided, we offer a 125 | // second way of providing options to your custom handler class. 126 | // 127 | // You can decorate a *method* with `@stateVar()` instead of a 128 | // variable. The method must return an object, and that object will 129 | // be assigned to the `options` object. 130 | // 131 | // Within the method you have access to the `this` context. So you 132 | // can access other properties and methods from your state class. 133 | // And you can add arrow function callbacks where you can access 134 | // `this`. This provides a lot of possibilities for a custom 135 | // handler class. 136 | if (options.propertyMethod && options.propertyMethod.kind === 'method') { 137 | Object.assign(options, options.propertyMethod.descriptor.value.call(this)); 138 | } 139 | 140 | } 141 | 142 | return options; 143 | 144 | } 145 | 146 | _recordRead(key) { 147 | stateRecorder.recordRead(this, key); 148 | } 149 | 150 | _notifyChange(key) { 151 | for (const observerObj of this._observers) { 152 | if (!observerObj.keys || observerObj.keys.includes(key)) { 153 | observerObj.observer(key); 154 | } 155 | }; 156 | } 157 | 158 | } 159 | 160 | 161 | export class StateVar { 162 | 163 | constructor(args) { 164 | this.options = args.options; // The options given in the `stateVar` declaration 165 | this.recordRead = args.recordRead; // Callback to indicate the `stateVar` is read 166 | this.notifyChange = args.notifyChange; // Callback to indicate the `stateVar` value has changed 167 | this.value = undefined; // The initial value 168 | } 169 | 170 | // Called when the `stateVar` on the `LitState` class is read (for example: 171 | // `myState.myStateVar`). Should return the value of the `stateVar`. 172 | get() { 173 | this.recordRead(); 174 | return this.value; 175 | } 176 | 177 | // Called before the `set()` method is called. If this method returns 178 | // `false`, the `set()` method won't be called. This can be used for 179 | // validation and/or optimization. 180 | shouldSetValue(value) { 181 | return this.value !== value; 182 | } 183 | 184 | // Called when the `stateVar` on the `LitState` class is set (for example: 185 | // `myState.myStateVar = 'value'`. 186 | set(value) { 187 | this.value = value; 188 | this.notifyChange(); 189 | } 190 | 191 | } 192 | 193 | 194 | export function stateVar(options = {}) { 195 | 196 | return element => { 197 | 198 | return { 199 | kind: 'field', 200 | key: Symbol(), 201 | placement: 'own', 202 | descriptor: {}, 203 | initializer() { 204 | if (typeof element.initializer === 'function') { 205 | this[element.key] = element.initializer.call(this); 206 | } 207 | }, 208 | finisher(litStateClass) { 209 | 210 | if (element.kind === 'method') { 211 | // You can decorate a *method* with `@stateVar()` instead 212 | // of a variable. When the state class is constructed, this 213 | // method will be called, and it's return value must be an 214 | // object that will be added to the options the stateVar 215 | // handler will receive. 216 | options.propertyMethod = element; 217 | } 218 | 219 | if (litStateClass.stateVarOptions === undefined) { 220 | litStateClass.stateVarOptions = {}; 221 | } 222 | 223 | litStateClass.stateVarOptions[element.key] = options; 224 | 225 | } 226 | }; 227 | 228 | }; 229 | 230 | } 231 | 232 | 233 | class StateRecorder { 234 | 235 | constructor() { 236 | this._log = null; 237 | } 238 | 239 | start() { 240 | this._log = new Map(); 241 | } 242 | 243 | recordRead(stateObj, key) { 244 | if (this._log === null) return; 245 | const keys = this._log.get(stateObj) || []; 246 | if (!keys.includes(key)) keys.push(key); 247 | this._log.set(stateObj, keys); 248 | } 249 | 250 | finish() { 251 | const stateVars = this._log; 252 | this._log = null; 253 | return stateVars; 254 | } 255 | 256 | } 257 | 258 | export const stateRecorder = new StateRecorder(); 259 | -------------------------------------------------------------------------------- /docs/src/state-handling/computed-values/computed-value-component.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, html } from 'lit-element'; 2 | import { demoComponentStyle } from '@app/demo-component.js'; 3 | import { observeState } from '@app/lit-state.js'; 4 | import { demoState } from './state'; 5 | import 'lit-docs'; 6 | 7 | 8 | @customElement('computed-value-component') 9 | export class ComputedValueComponent extends observeState(demoComponentStyle(LitElement)) { 10 | 11 | render() { 12 | 13 | return html` 14 | 15 | 16 | 17 |

<computed-value-component>

18 | 19 |

20 | Number 1: 21 | 26 |

27 | 28 |

29 | Number 2: 30 | 35 |

36 | 37 |

38 | 39 |

40 | 41 |

Sum: ${demoState.sum}

42 | 43 |
44 | 45 | `; 46 | 47 | } 48 | 49 | handleNumber1InputChange(event) { 50 | demoState.number1 = parseInt(event.target.value); 51 | } 52 | 53 | handleNumber2InputChange(event) { 54 | demoState.number2 = parseInt(event.target.value); 55 | } 56 | 57 | handleIncreaseBothButtonClick() { 58 | demoState.increaseBoth(); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /docs/src/state-handling/computed-values/index.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html } from 'lit-element'; 2 | import { LitDocsContent } from 'lit-docs'; 3 | import 'lit-docs'; 4 | import './computed-value-component.js'; 5 | 6 | 7 | @customElement('computed-values') 8 | export class ComputedValues extends LitDocsContent(LitElement) { 9 | 10 | render() { 11 | 12 | return html` 13 | 14 |

Computed values

15 | 16 |

17 | You can have helper methods on your 18 | LitState class that return computed 19 | values, or update multiple values at the same time. 20 |

21 | 22 |

23 | 24 |

25 | 26 |

27 | In our state class, we have a getter function that returns the 28 | sum of both numbers. Also there is a method that increases both 29 | numbers. 30 |

31 | 32 |

33 | 34 |

35 | 36 |

37 | Our component makes use of these helper methods, so that it 38 | doesn't have to do this work itself. 39 |

40 | 41 |

42 | 43 |

44 | 45 | `; 46 | 47 | } 48 | 49 | get demoStateCode() { 50 | 51 | return `import { LitState, stateVar } from 'lit-element-state.js'; 52 | 53 | class DemoState extends LitState { 54 | 55 | @stateVar() number1 = 0; 56 | @stateVar() number2 = 0; 57 | 58 | get sum() { 59 | return this.number1 + this.number2; 60 | } 61 | 62 | increaseBoth() { 63 | this.number1++; 64 | this.number2++; 65 | } 66 | 67 | } 68 | 69 | export const demoState = new DemoState();`; 70 | 71 | } 72 | 73 | get computedValueComponentCode() { 74 | 75 | return `import { customElement, LitElement, html } from 'lit-element'; 76 | import { observeState } from 'lit-element-state'; 77 | import { demoState } from './demo-state.js'; 78 | 79 | 80 | @customElement('computed-value-component') 81 | export class ComputedValueComponent extends observeState(LitElement) { 82 | 83 | render() { 84 | 85 | return html\` 86 | 87 |

<computed-value-component>

88 | 89 |

90 | Number 1: 91 | 96 |

97 | 98 |

99 | Number 2: 100 | 105 |

106 | 107 |

108 | 109 |

110 | 111 |

Sum: \${demoState.sum}

112 | 113 | \`; 114 | 115 | } 116 | 117 | handleNumber1InputChange(event) { 118 | demoState.number1 = parseInt(event.target.value); 119 | } 120 | 121 | handleNumber2InputChange(event) { 122 | demoState.number2 = parseInt(event.target.value); 123 | } 124 | 125 | handleIncreaseBothButtonClick() { 126 | demoState.increaseBoth(); 127 | } 128 | 129 | }`; 130 | 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /docs/src/state-handling/computed-values/state.js: -------------------------------------------------------------------------------- 1 | import { LitState, stateVar } from '@app/lit-state.js'; 2 | 3 | 4 | class DemoState extends LitState { 5 | 6 | @stateVar() number1 = 0; 7 | @stateVar() number2 = 0; 8 | 9 | get sum() { 10 | return this.number1 + this.number2; 11 | } 12 | 13 | increaseBoth() { 14 | this.number1++; 15 | this.number2++; 16 | } 17 | 18 | } 19 | 20 | 21 | export const demoState = new DemoState(); 22 | -------------------------------------------------------------------------------- /docs/src/state-handling/index.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html } from 'lit-element'; 2 | import { LitDocsContent } from 'lit-docs'; 3 | import 'lit-docs'; 4 | 5 | 6 | @customElement('state-handling') 7 | export class StateHandling extends LitDocsContent(LitElement) { 8 | 9 | render() { 10 | 11 | return html` 12 | 13 |

State handling

14 | 15 |

16 | Typically you would keep your state class (which extends from 17 | LitState) in a separate file, where the created 18 | instance of your state class is exported: 19 |

20 | 21 |

22 | 23 |

24 | 25 |

26 | Then you import the state in the component file where you need 27 | it. And use observeState() to decorate your components: 28 |

29 | 30 |

31 | 32 |

33 | 34 |

35 | But you could also have your state and all your components in 36 | one file if you like. Or do it any other way that works for 37 | you. 38 |

39 | 40 | `; 41 | 42 | } 43 | 44 | get stateCode() { 45 | 46 | return `import { LitState, stateVar } from 'lit-element-state'; 47 | 48 | class MyState extends LitState { 49 | @stateVar() myStateVar = 'myValue'; 50 | } 51 | 52 | export const myState = new MyState();`; 53 | 54 | } 55 | 56 | get componentCode() { 57 | 58 | return `import { LitElement, html } from 'lit-element'; 59 | import { observeState } from 'lit-element-state'; 60 | import { myState } from './state.js'; 61 | 62 | class MyComponent extends observeState(LitElement) { 63 | // .. 64 | }`; 65 | 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /docs/src/state-handling/nested-states/index.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html } from 'lit-element'; 2 | import { LitDocsContent } from 'lit-docs'; 3 | import 'lit-docs'; 4 | import './nested-state-component.js'; 5 | 6 | 7 | @customElement('nested-states') 8 | export class NestedStates extends LitDocsContent(LitElement) { 9 | 10 | render() { 11 | 12 | return html` 13 | 14 |

Nested States

15 | 16 |

17 | You can also easily nest states. 18 | stateVar variables can contain 19 | instances of other states. They will be recognized by LitState 20 | just the same. 21 |

22 | 23 |

Example:

24 | 25 |

26 | 27 |

28 | 29 |

30 | Note that there's only 1 31 | ParentState instance created, which is exported. 32 | There are 2 ChildState instances 33 | created, which are set on the parentState 34 | instance. The component imports the parentState, 35 | and accesses the child states from there. 36 |

37 | 38 |

39 | 40 |

41 | 42 |

Output:

43 | 44 |

45 | 46 |

47 | 48 | `; 49 | 50 | } 51 | 52 | get statesSourceCode() { 53 | 54 | return `import { LitState, stateVar } from 'lit-element-state'; 55 | 56 | class ParentState extends LitState { 57 | @stateVar() childState1; 58 | @stateVar() childState2; 59 | } 60 | 61 | class ChildState extends LitState { 62 | @stateVar() counter; 63 | } 64 | 65 | export const parentState = new ParentState(); 66 | const childState1 = new ChildState(); 67 | const childState2 = new ChildState(); 68 | 69 | childState1.counter = 1; 70 | childState2.counter = 1000; 71 | 72 | parentState.childState1 = childState1; 73 | parentState.childState2 = childState2;`; 74 | 75 | } 76 | 77 | get nestedStateComponentSourceCode() { 78 | 79 | return `import { customElement, LitElement, html } from 'lit-element'; 80 | import { observeState } from 'lit-element-state'; 81 | import { parentState } from './states.js'; 82 | 83 | @customElement('nested-state-component') 84 | export class NestedStateComponent extends observeState(LitElement) { 85 | 86 | render() { 87 | 88 | return html\` 89 | 90 |

<nested-state-component>

91 | 92 |

ChildState1 counter: \${parentState.childState1.counter}

93 | 94 | 95 |

ChildState2 counter: \${parentState.childState2.counter}

96 | 97 | 98 | \`; 99 | 100 | } 101 | 102 | handleIncreaseChild1CounterButtonClick() { 103 | parentState.childState1.counter++; 104 | } 105 | 106 | handleIncreaseChild2CounterButtonClick() { 107 | parentState.childState2.counter++; 108 | } 109 | 110 | }`; 111 | 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /docs/src/state-handling/nested-states/nested-state-component.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, html } from 'lit-element'; 2 | import { demoComponentStyle } from '@app/demo-component.js'; 3 | import { observeState } from '@app/lit-state.js'; 4 | import { parentState } from './states.js'; 5 | import 'lit-docs'; 6 | 7 | 8 | @customElement('nested-state-component') 9 | export class NestedStateComponent extends observeState(demoComponentStyle(LitElement)) { 10 | 11 | render() { 12 | 13 | return html` 14 | 15 | 16 | 17 |

<nested-state-component>

18 | 19 |

ChildState1 counter: ${parentState.childState1.counter}

20 | 21 | 22 |

ChildState2 counter: ${parentState.childState2.counter}

23 | 24 | 25 |
26 | 27 | `; 28 | 29 | } 30 | 31 | handleIncreaseChild1CounterButtonClick() { 32 | parentState.childState1.counter++; 33 | } 34 | 35 | handleIncreaseChild2CounterButtonClick() { 36 | parentState.childState2.counter++; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /docs/src/state-handling/nested-states/states.js: -------------------------------------------------------------------------------- 1 | import { LitState, stateVar } from '@app/lit-state.js'; 2 | 3 | 4 | class ParentState extends LitState { 5 | @stateVar() childState1; 6 | @stateVar() childState2; 7 | } 8 | 9 | 10 | class ChildState extends LitState { 11 | @stateVar() counter; 12 | } 13 | 14 | 15 | export const parentState = new ParentState(); 16 | const childState1 = new ChildState(); 17 | const childState2 = new ChildState(); 18 | 19 | childState1.counter = 1; 20 | childState2.counter = 1000; 21 | 22 | parentState.childState1 = childState1; 23 | parentState.childState2 = childState2; 24 | -------------------------------------------------------------------------------- /docs/src/use-cases/different-vars-on-rerender/changing-component.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, html, css } from 'lit-element'; 2 | import { demoComponentStyle } from '@app/demo-component.js'; 3 | import { observeState } from '@app/lit-state.js'; 4 | import { demoState } from './state.js'; 5 | import 'lit-docs'; 6 | 7 | 8 | @customElement('changing-component') 9 | export class ChangingComponent extends observeState(demoComponentStyle(LitElement)) { 10 | 11 | render() { 12 | 13 | return html` 14 | 15 | 16 | 17 |

<changing-component>

18 | 19 | 27 | 28 | 36 | 37 |

Value: ${this.counter}

38 | 39 |
40 | 41 | `; 42 | 43 | } 44 | 45 | get counter() { 46 | if (demoState.showCounter === 1) { 47 | return demoState.counter1; 48 | } else if (demoState.showCounter === 2) { 49 | return demoState.counter2; 50 | } 51 | } 52 | 53 | handleShowCounter1RadioClick() { 54 | demoState.showCounter = 1; 55 | } 56 | 57 | handleShowCounter2RadioClick() { 58 | demoState.showCounter = 2; 59 | } 60 | 61 | static get styles() { 62 | 63 | return css` 64 | 65 | label { 66 | display: block; 67 | margin: 5px 0; 68 | padding: 5px; 69 | background: #BBB; 70 | border-radius: 5px; 71 | cursor: pointer; 72 | } 73 | 74 | @media (prefers-color-scheme: dark) { 75 | 76 | label { 77 | background: #555; 78 | } 79 | 80 | } 81 | 82 | label input { 83 | margin: 0 5px 0; 84 | } 85 | 86 | `; 87 | 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /docs/src/use-cases/different-vars-on-rerender/control-component.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, html } from 'lit-element'; 2 | import { demoComponentStyle } from '@app/demo-component.js'; 3 | import { observeState } from '@app/lit-state.js'; 4 | import { demoState } from './state'; 5 | import 'lit-docs'; 6 | 7 | 8 | @customElement('control-component') 9 | export class ControlComponent extends observeState(demoComponentStyle(LitElement)) { 10 | 11 | render() { 12 | 13 | return html` 14 | 15 | 16 | 17 |

<control-component>

18 | 19 |

20 | Counter1: 21 | ${demoState.counter1} 22 | 23 |

24 | 25 |

26 | Counter2: 27 | ${demoState.counter2} 28 | 29 |

30 | 31 |
32 | 33 | `; 34 | 35 | } 36 | 37 | handleIncreaseCounter1ButtonClick() { 38 | demoState.counter1++; 39 | } 40 | 41 | handleIncreaseCounter2ButtonClick() { 42 | demoState.counter2++; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /docs/src/use-cases/different-vars-on-rerender/index.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html } from 'lit-element'; 2 | import { LitDocsContent } from 'lit-docs'; 3 | import 'lit-docs'; 4 | import './changing-component'; 5 | import './control-component'; 6 | 7 | 8 | @customElement('different-vars-on-rerender') 9 | export class DifferentVarsOnRerender extends LitDocsContent(LitElement) { 10 | 11 | render() { 12 | 13 | return html` 14 | 15 |

Different variables on re-render

16 | 17 |

Use case

18 | 19 |

20 | A component could render different stateVar 21 | variables at every re-render. Possible new 22 | stateVar variables should be observed so that the 23 | component re-renders when they change. 24 |

25 | 26 |

Execution

27 | 28 |

29 | LitState records the stateVar variables that are 30 | used in the component every time it re-renders. 31 |

32 | 33 |

Demonstration

34 | 35 |

36 | In this example, <changing-component> only 37 | shows 1 counter at a time, depending on the value of 38 | showCounter. 39 | <control-component> shows them both, and you 40 | can modify them there. 41 |

42 | 43 |
44 | 45 | 46 |
47 | 48 |

49 | 50 |

51 | 52 |

53 | 54 |

55 | 56 | `; 57 | 58 | } 59 | 60 | get changingComponentCode() { 61 | 62 | return `import { customElement, LitElement, html } from 'lit-element'; 63 | import { observeState } from 'lit-element-state'; 64 | import { demoState } from './demo-state.js'; 65 | 66 | 67 | @customElement('changing-component') 68 | export class ChangingComponent extends observeState(LitElement) { 69 | 70 | get counter() { 71 | if (demoState.showCounter === 1) { 72 | return demoState.counter1; 73 | } else if (demoState.showCounter === 2) { 74 | return demoState.counter2; 75 | } 76 | } 77 | 78 | render() { 79 | 80 | return html\` 81 | 82 |

<changing-component>

83 | 84 | 92 | 93 | 101 | 102 |

Counter: \${this.counter}

103 | 104 | \`; 105 | 106 | } 107 | 108 | handleShowCounter1RadioClick() { 109 | demoState.showCounter = 1; 110 | } 111 | 112 | handleShowCounter2RadioClick() { 113 | demoState.showCounter = 2; 114 | } 115 | 116 | }`; 117 | 118 | } 119 | 120 | 121 | get demoStateCode() { 122 | 123 | return `import { LitState, stateVar } from 'lit-element-state'; 124 | import { currentTime } from './utils.js' 125 | 126 | 127 | class DemoState extends LitState { 128 | @stateVar() showCounter = 1; 129 | @stateVar() counter1 = 0; 130 | @stateVar() counter2 = 0; 131 | } 132 | 133 | 134 | export const demoState = new DemoState();`; 135 | 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /docs/src/use-cases/different-vars-on-rerender/state.js: -------------------------------------------------------------------------------- 1 | import { LitState, stateVar } from '@app/lit-state.js'; 2 | 3 | 4 | class DemoState extends LitState { 5 | @stateVar() showCounter = 1; 6 | @stateVar() counter1 = 0; 7 | @stateVar() counter2 = 0; 8 | } 9 | 10 | 11 | export const demoState = new DemoState(); 12 | -------------------------------------------------------------------------------- /docs/src/use-cases/index.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html } from 'lit-element'; 2 | import { LitDocsContent } from 'lit-docs'; 3 | import 'lit-docs'; 4 | 5 | 6 | @customElement('use-cases') 7 | export class UseCases extends LitDocsContent(LitElement) { 8 | 9 | render() { 10 | 11 | return html` 12 | 13 |

Use cases

14 | 15 |

16 | This subsection contains demonstrations of specific use cases. 17 | And the way LitState covers those use cases. These pages also 18 | act as a way to test these use cases. 19 |

20 | 21 | `; 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /docs/src/use-cases/reconnected-components/index.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, property, html } from 'lit-element'; 2 | import { LitDocsContent } from 'lit-docs'; 3 | import 'lit-docs'; 4 | import { demoState } from './state'; 5 | import './reconnecting-component'; 6 | import './reconnect-control-component'; 7 | 8 | 9 | @customElement('reconnected-components') 10 | export class ReconnectedComponents extends LitDocsContent(LitElement) { 11 | 12 | firstUpdated() { 13 | super.firstUpdated(); 14 | this.initConnectionCallback(); 15 | } 16 | 17 | initConnectionCallback() { 18 | 19 | this.reconnectingComponent = this.shadowRoot.getElementById('reconnectingComponent'); 20 | this.demoComponentsContainer = this.shadowRoot.getElementById('demoComponentsContainer'); 21 | 22 | demoState.addObserver(() => this.initReconnectingComponentConnection(), ['connected']); 23 | 24 | } 25 | 26 | initReconnectingComponentConnection() { 27 | if (demoState.connected) { 28 | this.demoComponentsContainer.appendChild(this.reconnectingComponent); 29 | } else { 30 | this.demoComponentsContainer.removeChild(this.reconnectingComponent); 31 | } 32 | } 33 | 34 | render() { 35 | 36 | return html` 37 | 38 |

Reconnected components

39 | 40 |

Use case

41 | 42 |
    43 |
  1. A component that is observing some stateVar variables is rendered to the DOM.
  2. 44 |
  3. The component gets dynamically removed from the DOM.
  4. 45 |
  5. The state possibly changes.
  6. 46 |
  7. The same component instance gets added to the DOM again.
  8. 47 |
48 | 49 |

Execution

50 | 51 |
    52 |
  • 53 | When the component gets removed from the DOM 54 | (disconnectedCallback()), the observers are 55 | removed. 56 |
  • 57 |
  • 58 | When it gets added to the DOM again 59 | (connectedCallback()), the component is 60 | rerendered again so the observers get added again. 61 |
  • 62 |
63 | 64 |

Demo

65 | 66 |
67 | 68 | 69 |
70 | 71 | `; 72 | 73 | } 74 | 75 | get demoStateCode() { 76 | 77 | return `labberr`; 78 | 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /docs/src/use-cases/reconnected-components/reconnect-control-component.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, html } from 'lit-element'; 2 | import { demoComponentStyle } from '@app/demo-component.js'; 3 | import { observeState } from '@app/lit-state.js'; 4 | import { demoState } from './state'; 5 | import 'lit-docs'; 6 | 7 | 8 | @customElement('reconnect-control-component') 9 | export class ReconnectControlComponent extends observeState(demoComponentStyle(LitElement)) { 10 | 11 | render() { 12 | 13 | return html` 14 | 15 | 16 | 17 |

<control-component>

18 |

Counter: ${demoState.counter}

19 | 20 |
21 | 22 | 23 | 24 | 30 | 31 | 37 | 38 |
39 | 40 |
41 | 42 | `; 43 | 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /docs/src/use-cases/reconnected-components/reconnecting-component.js: -------------------------------------------------------------------------------- 1 | import { customElement, LitElement, html } from 'lit-element'; 2 | import { demoComponentStyle } from '@app/demo-component.js'; 3 | import { observeState } from '@app/lit-state.js'; 4 | import { demoState } from './state'; 5 | import 'lit-docs'; 6 | 7 | 8 | @customElement('reconnecting-component') 9 | export class ReconnectingComponent extends observeState(demoComponentStyle(LitElement)) { 10 | 11 | render() { 12 | return html` 13 | 14 |

<reconnecting-component>

15 |

Counter: ${demoState.counter}

16 |
17 | `; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /docs/src/use-cases/reconnected-components/state.js: -------------------------------------------------------------------------------- 1 | import { LitState, stateVar } from '@app/lit-state.js'; 2 | 3 | 4 | class DemoState extends LitState { 5 | @stateVar() counter = 0; 6 | @stateVar() connected = true; 7 | } 8 | 9 | 10 | export const demoState = new DemoState(); 11 | -------------------------------------------------------------------------------- /lit-state.js: -------------------------------------------------------------------------------- 1 | export const observeState = superclass => class extends superclass { 2 | 3 | constructor() { 4 | super(); 5 | this._observers = []; 6 | } 7 | 8 | update(changedProperties) { 9 | stateRecorder.start(); 10 | super.update(changedProperties); 11 | this._initStateObservers(); 12 | } 13 | 14 | connectedCallback() { 15 | super.connectedCallback(); 16 | if (this._wasConnected) { 17 | this.requestUpdate(); 18 | delete this._wasConnected; 19 | } 20 | } 21 | 22 | disconnectedCallback() { 23 | super.disconnectedCallback(); 24 | this._wasConnected = true; 25 | this._clearStateObservers(); 26 | } 27 | 28 | _initStateObservers() { 29 | this._clearStateObservers(); 30 | if (!this.isConnected) return; 31 | this._addStateObservers(stateRecorder.finish()); 32 | } 33 | 34 | _addStateObservers(stateVars) { 35 | for (let [state, keys] of stateVars) { 36 | const observer = () => this.requestUpdate(); 37 | this._observers.push([state, observer]); 38 | state.addObserver(observer, keys); 39 | } 40 | } 41 | 42 | _clearStateObservers() { 43 | for (let [state, observer] of this._observers) { 44 | state.removeObserver(observer); 45 | } 46 | this._observers = []; 47 | } 48 | 49 | } 50 | 51 | 52 | export class LitState { 53 | 54 | constructor() { 55 | this._observers = []; 56 | this._initStateVars(); 57 | } 58 | 59 | addObserver(observer, keys) { 60 | this._observers.push({observer, keys}); 61 | } 62 | 63 | removeObserver(observer) { 64 | this._observers = this._observers.filter(observerObj => observerObj.observer !== observer); 65 | } 66 | 67 | _initStateVars() { 68 | 69 | if (this.constructor.stateVarOptions) { 70 | for (let [key, options] of Object.entries(this.constructor.stateVarOptions)) { 71 | this._initStateVar(key, options); 72 | } 73 | } 74 | 75 | if (this.constructor.stateVars) { 76 | for (let [key, value] of Object.entries(this.constructor.stateVars)) { 77 | this._initStateVar(key, {}); 78 | this[key] = value; 79 | } 80 | } 81 | 82 | } 83 | 84 | _initStateVar(key, options) { 85 | 86 | if (this.hasOwnProperty(key)) { 87 | // Property already defined, so don't re-define. 88 | return; 89 | } 90 | 91 | options = this._parseOptions(options); 92 | 93 | const stateVar = new options.handler({ 94 | options: options, 95 | recordRead: () => this._recordRead(key), 96 | notifyChange: () => this._notifyChange(key) 97 | }); 98 | 99 | Object.defineProperty( 100 | this, 101 | key, 102 | { 103 | get() { 104 | return stateVar.get(); 105 | }, 106 | set(value) { 107 | if (stateVar.shouldSetValue(value)) { 108 | stateVar.set(value); 109 | } 110 | }, 111 | configurable: true, 112 | enumerable: true 113 | } 114 | ); 115 | 116 | } 117 | 118 | _parseOptions(options) { 119 | 120 | if (!options.handler) { 121 | options.handler = StateVar; 122 | } else { 123 | 124 | // In case of a custom `StateVar` handler is provided, we offer a 125 | // second way of providing options to your custom handler class. 126 | // 127 | // You can decorate a *method* with `@stateVar()` instead of a 128 | // variable. The method must return an object, and that object will 129 | // be assigned to the `options` object. 130 | // 131 | // Within the method you have access to the `this` context. So you 132 | // can access other properties and methods from your state class. 133 | // And you can add arrow function callbacks where you can access 134 | // `this`. This provides a lot of possibilities for a custom 135 | // handler class. 136 | if (options.propertyMethod && options.propertyMethod.kind === 'method') { 137 | Object.assign(options, options.propertyMethod.descriptor.value.call(this)); 138 | } 139 | 140 | } 141 | 142 | return options; 143 | 144 | } 145 | 146 | _recordRead(key) { 147 | stateRecorder.recordRead(this, key); 148 | } 149 | 150 | _notifyChange(key) { 151 | for (const observerObj of this._observers) { 152 | if (!observerObj.keys || observerObj.keys.includes(key)) { 153 | observerObj.observer(key); 154 | } 155 | }; 156 | } 157 | 158 | } 159 | 160 | 161 | export class StateVar { 162 | 163 | constructor(args) { 164 | this.options = args.options; // The options given in the `stateVar` declaration 165 | this.recordRead = args.recordRead; // Callback to indicate the `stateVar` is read 166 | this.notifyChange = args.notifyChange; // Callback to indicate the `stateVar` value has changed 167 | this.value = undefined; // The initial value 168 | } 169 | 170 | // Called when the `stateVar` on the `LitState` class is read (for example: 171 | // `myState.myStateVar`). Should return the value of the `stateVar`. 172 | get() { 173 | this.recordRead(); 174 | return this.value; 175 | } 176 | 177 | // Called before the `set()` method is called. If this method returns 178 | // `false`, the `set()` method won't be called. This can be used for 179 | // validation and/or optimization. 180 | shouldSetValue(value) { 181 | return this.value !== value; 182 | } 183 | 184 | // Called when the `stateVar` on the `LitState` class is set (for example: 185 | // `myState.myStateVar = 'value'`. 186 | set(value) { 187 | this.value = value; 188 | this.notifyChange(); 189 | } 190 | 191 | } 192 | 193 | 194 | export function stateVar(options = {}) { 195 | 196 | return element => { 197 | 198 | return { 199 | kind: 'field', 200 | key: Symbol(), 201 | placement: 'own', 202 | descriptor: {}, 203 | initializer() { 204 | if (typeof element.initializer === 'function') { 205 | this[element.key] = element.initializer.call(this); 206 | } 207 | }, 208 | finisher(litStateClass) { 209 | 210 | if (element.kind === 'method') { 211 | // You can decorate a *method* with `@stateVar()` instead 212 | // of a variable. When the state class is constructed, this 213 | // method will be called, and it's return value must be an 214 | // object that will be added to the options the stateVar 215 | // handler will receive. 216 | options.propertyMethod = element; 217 | } 218 | 219 | if (litStateClass.stateVarOptions === undefined) { 220 | litStateClass.stateVarOptions = {}; 221 | } 222 | 223 | litStateClass.stateVarOptions[element.key] = options; 224 | 225 | } 226 | }; 227 | 228 | }; 229 | 230 | } 231 | 232 | 233 | class StateRecorder { 234 | 235 | constructor() { 236 | this._log = null; 237 | } 238 | 239 | start() { 240 | this._log = new Map(); 241 | } 242 | 243 | recordRead(stateObj, key) { 244 | if (this._log === null) return; 245 | const keys = this._log.get(stateObj) || []; 246 | if (!keys.includes(key)) keys.push(key); 247 | this._log.set(stateObj, keys); 248 | } 249 | 250 | finish() { 251 | const stateVars = this._log; 252 | this._log = null; 253 | return stateVars; 254 | } 255 | 256 | } 257 | 258 | export const stateRecorder = new StateRecorder(); 259 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lit-element-state", 3 | "version": "1.5.2", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lit-element-state", 3 | "version": "1.7.0", 4 | "description": "Simple shared app state management for LitElement", 5 | "main": "lit-state.js", 6 | "files": [ 7 | "lit-state.js" 8 | ], 9 | "peerDependencies": { 10 | "lit-element": "^2.4.0" 11 | }, 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/gitaarik/lit-state.git" 18 | }, 19 | "keywords": [ 20 | "state", 21 | "management", 22 | "lit-element", 23 | "LitElement", 24 | "app", 25 | "shared", 26 | "global", 27 | "observer", 28 | "observable" 29 | ], 30 | "author": "gitaarik", 31 | "license": "LGPL-3.0-or-later", 32 | "bugs": { 33 | "url": "https://github.com/gitaarik/lit-state/issues" 34 | }, 35 | "homepage": "https://github.com/gitaarik/lit-state#readme" 36 | } 37 | --------------------------------------------------------------------------------