1- import { type ReactiveController , type ReactiveControllerHost } from "lit" ;
1+ import { type ReactiveController , type ReactiveElement } from "lit" ;
2+ import isEqual from "lodash/isEqual" ;
23
34import { SearchParamsController } from "@/controllers/searchParams" ;
45
@@ -23,13 +24,32 @@ import { SearchParamsController } from "@/controllers/searchParams";
2324 * `setValue` will update the URL search params and push state to the history
2425 * stack, and reading it is cached, so performance should be good no matter what.
2526 *
26- * There is currently one option available via a third parameter, `initial`,
27- * which can be used to modify the initial value based on the URL search params
28- * at the time of initialization. This is useful if there's a default value that
29- * should be used if the URL search params are empty, but otherwise the value
30- * from the URL should be prioritized, for example.
27+ * Updates to the value via `setValue` will request an update from the host
28+ * with the name set to the property name of the controller on the host followed
29+ * by `.setValue`. This allows you to use lifecycle hooks such as `willUpdate`
30+ * you would with any other state or property.
3131 *
32- * Example usage:
32+ * Similarly, updates to the value originating from the browser (e.g. user
33+ * navigation, URL changes, etc.) will also trigger updates to the value, but
34+ * suffixed with `.value`. This allows you to use lifecycle hooks to react to
35+ * changes originating in different places differently.
36+ *
37+ * ## Options
38+ *
39+ * ### `initial`
40+ * `options.initial` can be used to modify the initial value based on the URL
41+ * search params at the time of initialization. This is useful if there's a
42+ * default value that should be used if the URL search params are empty, but
43+ * otherwise the value from the URL should be prioritized, for example.
44+ *
45+ * ### `propertyName`
46+ * `options.propertyName` can be used to specify the name of the property in the
47+ * host so that this controller can correctly request updates in the host. This
48+ * shouldn't need to be set if the controller is initialized in the host class
49+ * body, but if it's initialized in the host class constructor, it should be set
50+ * to the name of the property that holds the controller instance.
51+ *
52+ * ## Example usage
3353 * ```ts
3454 * class MyComponent extends LitElement {
3555 * query = new SearchParamsValue<string>(
@@ -45,49 +65,96 @@ import { SearchParamsController } from "@/controllers/searchParams";
4565 * (params) => params.get("q") ?? ""
4666 * );
4767 * render() {
48- * return html`<input .value=${this.query.value} @input =${(e) => {this.query.setValue(e.target.value)}} />`;
68+ * return html`<input
69+ * .value=${this.query.value}
70+ * @input =${(e) => {this.query.setValue(e.target.value)}}
71+ * />`;
4972 * }
5073 * }
5174 * ```
5275 */
5376export class SearchParamsValue < T > implements ReactiveController {
54- host : ReactiveControllerHost ;
77+ readonly #host: ReactiveElement ;
78+
79+ #value: T ;
5580
56- private _value : T ;
81+ readonly #searchParams;
82+ readonly #encoder: ( value : T , params : URLSearchParams ) => URLSearchParams ;
83+ readonly #decoder: ( params : URLSearchParams ) => T ;
5784
58- private readonly searchParams ;
59- private readonly encoder : (
60- value : T ,
61- params : URLSearchParams ,
62- ) => URLSearchParams ;
63- private readonly decoder : ( params : URLSearchParams ) => T ;
85+ readonly #options: {
86+ initial ?: ( valueFromSearchParams ?: T ) => T ;
87+ propertyKey ?: PropertyKey ;
88+ } ;
6489
6590 public get value ( ) : T {
66- return this . _value ;
91+ return this . #value ;
6792 }
93+ /**
94+ * Set value and request update if deep comparison with current value is not equal.
95+ */
6896 public setValue ( value : T ) {
69- this . _value = value ;
70- this . searchParams . update ( ( params ) => this . encoder ( value , params ) ) ;
71- this . host . requestUpdate ( ) ;
97+ if ( isEqual ( value , this . #value) ) return ;
98+
99+ const oldValue = this . #value;
100+ this . #value = value ;
101+ this . #searchParams. update ( ( params ) => this . #encoder( value , params ) ) ;
102+ this . #host. requestUpdate ( this . #getPropertyName( "setValue" ) , oldValue ) ;
103+ }
104+ // Little bit hacky/metaprogramming-y, but this lets us auto-detect property
105+ // name from the host element's properties without needing to repeat the name
106+ // in a string passed into options in order to have `requestUpdate` be called
107+ // with the correct property name. Ideally, eventually we'd use a decorator,
108+ // which would allow us to avoid this hacky approach — though that might make
109+ // differentiating between internally-triggered ("setValue") and
110+ // externally-triggered ("value") updates a bit more complex.
111+ #getPropertyName( type : "value" | "setValue" ) : PropertyKey | undefined {
112+ // Use explicit property name if provided
113+ if ( this . #options. propertyKey )
114+ return `${ this . #options. propertyKey . toString ( ) } .${ type } ` ;
115+
116+ try {
117+ for ( const prop of Reflect . ownKeys ( this . #host) ) {
118+ const descriptor = Object . getOwnPropertyDescriptor ( this . #host, prop ) ;
119+ if ( descriptor && descriptor . value === this ) {
120+ this . #options. propertyKey = prop ;
121+ return `${ prop . toString ( ) } .${ type } ` ;
122+ }
123+ }
124+ } catch ( error ) {
125+ console . debug (
126+ "SearchParamsValue: Failed to auto-detect property name" ,
127+ error ,
128+ ) ;
129+ }
130+ return undefined ;
72131 }
73132
74133 constructor (
75- host : ReactiveControllerHost ,
134+ host : ReactiveElement ,
76135 encoder : ( value : T , params : URLSearchParams ) => URLSearchParams ,
77136 decoder : ( params : URLSearchParams ) => T ,
78- options ?: { initial ?: ( valueFromSearchParams ?: T ) => T } ,
137+ options ?: {
138+ initial ?: ( valueFromSearchParams ?: T ) => T ;
139+ propertyKey ?: PropertyKey ;
140+ } ,
79141 ) {
80- ( this . host = host ) . addController ( this ) ;
81- this . encoder = encoder ;
82- this . decoder = decoder ;
83- this . searchParams = new SearchParamsController ( this . host , ( params ) => {
84- this . _value = this . decoder ( params ) ;
85- this . host . requestUpdate ( ) ;
142+ ( this . #host = host ) . addController ( this ) ;
143+ this . #encoder = encoder ;
144+ this . #decoder = decoder ;
145+ this . #options = options || { } ;
146+ this . #searchParams = new SearchParamsController ( this . #host, ( params ) => {
147+ const oldValue = this . #value;
148+ this . #value = this . #decoder( params ) ;
149+
150+ if ( isEqual ( oldValue , this . #value) ) return ;
151+
152+ this . #host. requestUpdate ( this . #getPropertyName( "value" ) , oldValue ) ;
86153 } ) ;
87154
88- this . _value = options ?. initial
89- ? options . initial ( this . decoder ( this . searchParams . searchParams ) )
90- : this . decoder ( this . searchParams . searchParams ) ;
155+ this . #value = options ?. initial
156+ ? options . initial ( this . # decoder( this . # searchParams. searchParams ) )
157+ : this . # decoder( this . # searchParams. searchParams ) ;
91158 }
92159
93160 hostConnected ( ) { }
0 commit comments