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}