001package arez.dom;
002
003import akasha.EventListener;
004import akasha.EventTarget;
005import arez.ComputableValue;
006import arez.Disposable;
007import arez.annotations.Action;
008import arez.annotations.ArezComponent;
009import arez.annotations.ComputableValueRef;
010import arez.annotations.DepType;
011import arez.annotations.Feature;
012import arez.annotations.Memoize;
013import arez.annotations.Observable;
014import arez.annotations.OnActivate;
015import arez.annotations.OnDeactivate;
016import java.util.Objects;
017import javax.annotation.Nonnull;
018import jsinterop.annotations.JsFunction;
019
020/**
021 * Generic component that exposes a property as observable where changes to the variable are signalled
022 * using a browser event. A typical example is making the value of <code>window.innerWidth</code>
023 * observable by listening to <code>"resize"</code> events on the window. This could be achieved with code such
024 * as:
025 *
026 * <pre>{@code
027 * EventDrivenValue<Window, Integer> innerWidth = EventDrivenValue.create( window, "resize", () -> window.innerWidth )
028 * }</pre>
029 *
030 * <p>It is important that the code not add a listener to the underlying event source until there is an
031 * observer accessing the <code>"value"</code> observable defined by the EventDrivenValue class. The first
032 * observer that observes the observable will result in an event listener being added to the event source
033 * and this listener will not be removed until there is no observers left observing the value. This means
034 * that a component that is not being used has very little overhead.</p>
035 *
036 * @param <SourceType> the type of the DOM element that generates events of interest.
037 * @param <ValueType>  the type of the value returned by the "value" observable.
038 */
039@ArezComponent( requireId = Feature.DISABLE, disposeNotifier = Feature.DISABLE )
040public abstract class EventDrivenValue<SourceType extends EventTarget, ValueType>
041{
042  /**
043   * The functional interface defining accessor.
044   *
045   * @param <SourceType> the type of the DOM element that generates events of interest.
046   * @param <ValueType>  the type of the value returned by the "value" observable.
047   */
048  @FunctionalInterface
049  @JsFunction
050  public interface Accessor<SourceType extends EventTarget, ValueType>
051  {
052    /**
053     * Return the value.
054     *
055     * @param source the source that drives the access.
056     * @return the value
057     */
058    ValueType get( @Nonnull SourceType source );
059  }
060
061  /**
062   * The
063   */
064  @Nonnull
065  private final EventListener _listener = e -> onEvent();
066  @Nonnull
067  private SourceType _source;
068  @Nonnull
069  private final String _event;
070  @Nonnull
071  private final Accessor<SourceType, ValueType> _getter;
072  private boolean _active;
073
074  /**
075   * Create the component.
076   *
077   * @param <SourceType> the type of the DOM element that generates events of interest.
078   * @param <ValueType>  the type of the value returned by the "value" observable.
079   * @param source       the DOM element that generates events of interest.
080   * @param event        the event type that could result in changes to the observed value. The event type is expected to be generated by the source element.
081   * @param getter       the function that retrieves the observed value from the platform.
082   * @return the new component.
083   */
084  @Nonnull
085  public static <SourceType extends EventTarget, ValueType>
086  EventDrivenValue<SourceType, ValueType> create( @Nonnull final SourceType source,
087                                                  @Nonnull final String event,
088                                                  @Nonnull final Accessor<SourceType, ValueType> getter )
089  {
090    return new Arez_EventDrivenValue<>( source, event, getter );
091  }
092
093  EventDrivenValue( @Nonnull final SourceType source,
094                    @Nonnull final String event,
095                    @Nonnull final Accessor<SourceType, ValueType> getter )
096  {
097    _source = Objects.requireNonNull( source );
098    _event = Objects.requireNonNull( event );
099    _getter = Objects.requireNonNull( getter );
100  }
101
102  /**
103   * Return the element that generates the events that report potential changes to the observed value.
104   *
105   * @return the associated element.
106   */
107  @Nonnull
108  @Observable
109  public SourceType getSource()
110  {
111    return _source;
112  }
113
114  /**
115   * Set the element that generates events.
116   * This ensures that the event listeners are managed correctly if the source is currently being observed.
117   *
118   * @param source the the event source.
119   */
120  public void setSource( @Nonnull final SourceType source )
121  {
122    if ( _active )
123    {
124      unbindListener();
125    }
126    _source = source;
127    if ( _active )
128    {
129      bindListener();
130    }
131  }
132
133  /**
134   * Return the value.
135   *
136   * @return the value.
137   */
138  @Memoize( depType = DepType.AREZ_OR_EXTERNAL )
139  public ValueType getValue()
140  {
141    // Deliberately observing source via getSource() so that this method re-runs
142    // when source changes
143    return _getter.get( getSource() );
144  }
145
146  @ComputableValueRef
147  abstract ComputableValue<?> getValueComputableValue();
148
149  /**
150   * Hook invoked when the value moves from unobserved to observed.
151   * Adds underlying listener.
152   */
153  @OnActivate
154  void onValueActivate()
155  {
156    _active = true;
157    bindListener();
158  }
159
160  /**
161   * Hook invoked when value is no longer observed.
162   * Removes underlying listener.
163   */
164  @OnDeactivate
165  void onValueDeactivate()
166  {
167    _active = false;
168    unbindListener();
169  }
170
171  private void onEvent()
172  {
173    // Due to bugs (?) or perhaps "implementation choices" in some browsers, an event can be delivered
174    // after listener is removed. According to notes in https://github.com/ReactTraining/react-media/blob/master/modules/MediaQueryList.js
175    // Safari doesn't clear up listener queue on MediaQueryList when removeListener is called if there
176    // is already waiting in the internal event queue.
177    //
178    // To avoid a potential crash when invariants are enabled or indeterminate behaviour when invariants
179    // are not enabled, a guard has been added.
180    if ( Disposable.isNotDisposed( this ) )
181    {
182      notifyOnChange();
183    }
184  }
185
186  /**
187   * Hook invoked from listener to indicate  memoized value should be recomputed.
188   */
189  @Action
190  void notifyOnChange()
191  {
192    getValueComputableValue().reportPossiblyChanged();
193  }
194
195  /**
196   * Add underlying listener to source.
197   */
198  private void bindListener()
199  {
200    _source.addEventListener( _event, _listener );
201  }
202
203  /**
204   * Remove underlying listener from source.
205   */
206  private void unbindListener()
207  {
208    _source.removeEventListener( _event, _listener );
209  }
210}