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