Open csandeep opened 7 months ago
Thanks a lot for reporting this @csandeep - Can you tell me which version of lit you are running this code with? I have not yet tested it with latest TS (https://lit.dev/docs/components/decorators/#decorators-typescript) and would suspect that TS compile option have some kind of influence here.
Planning to to some cleanup (there are a couple of PR I'd like to accept) and further tests against latest version of lit and decorators this week. I will also look into this issue.
as per @hook, this is the one I use to sync a state with a firebase database:
/**
* A hook for @lit-app/state synchronizing with firebase
*/
import { set, child, onValue, DatabaseReference, DataSnapshot, Unsubscribe, Query } from 'firebase/database'
import { Hook, State } from '@lit-app/state'
// TODO: Ref should be DatabaseReference | Map<string, DatabaseReference>
type Ref = DatabaseReference | { [key: string]: DatabaseReference } | undefined
type hookDef = {
path?: string,
forceSet?: boolean
}
/** DatabaseReference is an interface. We cannot use value instanceof DatabaseReference */
const isRef = (value: Ref) => {
return value && value.key && value.root && value.parent;
};
const hookName = 'firebase'
/**
* Firebase hook to synchronize state values with a firebase path
*
* The path resolves relatively to a Database reference set to the hook.
*
* Synchronisation works like this:
*
* When the ref is set:
* If the value of the database does not exits (onValue returning null), or the hook property has `forceSet` set to `true` set with current state value
* Otherwise (database value does exist, no `forceSet`), read the value from the database first
*
* Then database write updates on state value and vice-versa.
*
* The Database reference is either :
* 1. one ref (applying to all hooked properties), or
* 2. a <key, ref> object, with individual ref applying to hooked properties
*
*
* Examples:
*
* Same ref for the all state properties
* ```js
* @hook(firebase, {path: /mypath})
* @property() a // property `a` will sync with child(state.ref, 'mypath')
* @hook(firebase )
* @property() name // property `name` will sync with child(state.ref, 'name')
* ```
*
* setting a <key,ref> object
*
* ```js
* const ref = {
* a: ref(getDatabase(), '/a/path/for/a')
* name: ref(getDatabase(), '/a/path/for/name')
* }
* @hook(firebase, {path: /mypath})
* @property() a // property `a` will sync with with path '/a/path/for/a' (mypath is not taken into account)
* @hook(firebase )
* @property() name // property `name` will sync path '/a/path/for/name'
* ```
*
*/
export class HookFirebase extends Hook {
static override hookName: string = hookName;
private _ref!: Ref
private _hasSynched: Map<string, boolean> = new Map()
private _unsubscribe: Unsubscribe[] = []
set ref(ref: Ref) {
this._unsubscribe.forEach(unsubscribe => unsubscribe())
this._unsubscribe = []
this._hasSynched = new Map()
if (ref) {
this._ref = ref;
const callback = (key: string) => (snap: DataSnapshot) => {
const value = snap.val()
const definition = this.getDefinition(key)
const hookDef = definition?.hook?.firebase as hookDef
const stateValue = this.state[key as keyof State]
// consider the value has been synched if it is the same value as the one in the state
if (value !== null && (JSON.stringify(value) === JSON.stringify(stateValue))) {
this._hasSynched.set(key, true)
}
else if (!this._hasSynched.get(key) &&
// skipAsync is set to true when the value is set by a query parameter
(value === null || hookDef.forceSet || definition?.skipAsync) && stateValue !== undefined) {
set(snap.ref, stateValue)
.then(() => this._hasSynched.set(key, true))
} else {
this.toState({ [key]: snap.val() })
this._hasSynched.set(key, true)
}
}
this.hookedProps.forEach(([key, definition]) => {
const hookDef = definition?.hook?.firebase as hookDef
// @ts-ignore
const r = isRef(this.ref[key]) ? this._ref[key] :
isRef(this.ref) ? child(this.ref as DatabaseReference, hookDef?.path || key) :
null
if (r) {
this._unsubscribe.push(onValue(r as Query, callback(key), (error) => {
console.error(error)
}))
}
})
}
}
get ref(): Ref {
return this._ref
}
store() {
if (isRef(this._ref)) {
const stateValue = this.state.stateValue
const value = {}
this.hookedProps.forEach(([key, definition]) => {
const hookDef = definition?.hook?.firebase as hookDef
const v = stateValue[key]
// @ts-ignore
if(v !== undefined) {value[hookDef?.path || key] = v}
})
set(this._ref as DatabaseReference, value)
}
}
constructor(public override state: State, ref?: Ref) {
super(state)
if (ref) {
this.ref = ref
}
}
override fromState(key: string, value: unknown): void {
// console.info('fromState', key, value)
if (value !== undefined && this._hasSynched.get(key) && this.ref) {
const definition = this.getDefinition(key)
const isKeyedRef = isRef((this.ref as { [key: string]: DatabaseReference })[key])
set(
//@ts-ignore
isKeyedRef ? (this.ref[key] as DatabaseReference) :
child(this.ref as DatabaseReference,
(definition?.hook?.firebase as hookDef)?.path || key), value
)
}
}
override reset():void {
this.ref = undefined
}
}
Thank you @christophe-g , I'm using lit v3.1.0
@csandeep - released a new version which now supports lit ^3 and @lit/reactive-element@^2.
That should fix your issue.
Persisting an object to localSorage results in storing the object's toString() value, which is a
[object Object]
. Here's my codedata-types.ts
:app-state.ts
:I have followed the
@hook
documentation but couldn't figure out how to, any hints ?