Skip to content

Commit

Permalink
feat(directives): Export directives using exportAs: for use as temp…
Browse files Browse the repository at this point in the history
…late variables

Closes #149
  • Loading branch information
christopherthielen committed Nov 5, 2017
1 parent 8f16179 commit 3d532b6
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 66 deletions.
9 changes: 5 additions & 4 deletions src/directives/uiSref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export class AnchorUISref {
*/
@Directive({
selector: '[uiSref]',
exportAs: 'uiSref',
host: { '(click)': 'go()' }
})
export class UISref implements OnChanges {
Expand Down Expand Up @@ -109,7 +110,7 @@ export class UISref implements OnChanges {
/** @internalapi */ private _statesSub: Subscription;
/** @internalapi */ private _router: UIRouter;
/** @internalapi */ private _anchorUISref: AnchorUISref;
/** @internalapi */ public parent: ParentUIViewInject;
/** @internalapi */ private _parent: ParentUIViewInject;

constructor(
_router: UIRouter,
Expand All @@ -118,7 +119,7 @@ export class UISref implements OnChanges {
) {
this._router = _router;
this._anchorUISref = _anchorUISref;
this.parent = parent;
this._parent = parent;

this._statesSub = _router.globals.states$.subscribe(() => this.update());
}
Expand All @@ -145,7 +146,7 @@ export class UISref implements OnChanges {
this.targetState$.unsubscribe();
}

update() {
private update() {
let $state = this._router.stateService;
if (this._emit) {
let newTarget = $state.target(this.state, this.params, this.getOptions());
Expand All @@ -160,7 +161,7 @@ export class UISref implements OnChanges {

getOptions() {
let defaultOpts: TransitionOptions = {
relative: this.parent && this.parent.context && this.parent.context.name,
relative: this._parent && this._parent.context && this._parent.context.name,
inherit: true ,
source: "sref"
};
Expand Down
11 changes: 7 additions & 4 deletions src/directives/uiSrefStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,15 @@ function mergeSrefStatus(left: SrefStatus, right: SrefStatus): SrefStatus {
*
* This API is subject to change.
*/
@Directive({ selector: '[uiSrefStatus],[uiSrefActive],[uiSrefActiveEq]' })
@Directive({
selector: '[uiSrefStatus],[uiSrefActive],[uiSrefActiveEq]',
exportAs: 'uiSrefStatus'
})
export class UISrefStatus {
/** current statuses of the state/params the uiSref directive is linking to */
@Output("uiSrefStatus") uiSrefStatus = new EventEmitter<SrefStatus>(false);
/** Monitor all child components for UISref(s) */
@ContentChildren(UISref, {descendants: true}) srefs: QueryList<UISref>;
@ContentChildren(UISref, {descendants: true}) private _srefs: QueryList<UISref>;

/** The current status */
status: SrefStatus;
Expand Down Expand Up @@ -218,8 +221,8 @@ export class UISrefStatus {
// Watch the @ContentChildren UISref[] components and get their target states

// let srefs$: Observable<UISref[]> = of(this.srefs.toArray()).concat(this.srefs.changes);
this._srefs$ = new BehaviorSubject(this.srefs.toArray());
this._srefChangesSub = this.srefs.changes.subscribe(srefs => this._srefs$.next(srefs));
this._srefs$ = new BehaviorSubject(this._srefs.toArray());
this._srefChangesSub = this._srefs.changes.subscribe(srefs => this._srefs$.next(srefs));

let targetStates$: Observable<TargetState[]> =
switchMap.call(this._srefs$, (srefs: UISref[]) =>
Expand Down
100 changes: 42 additions & 58 deletions src/directives/uiView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,40 +87,25 @@ const ng2ComponentInputs = (factory: ComponentFactory<any>): InputMapping[] => {
*/
@Component({
selector: 'ui-view, [ui-view]',
exportAs: 'uiView',
template: `
<ng-template #componentTarget></ng-template>
<ng-content *ngIf="!componentRef"></ng-content>
<ng-content *ngIf="!_componentRef"></ng-content>
`
// styles: [`
// .done-true {
// text-decoration: line-through;
// color: grey;
// }`
// ],
// template: `
// <div style="padding: 1em; border: 1px solid lightgrey;">
//
// <div #content style="color: lightgrey; font-size: smaller;">
// <div>ui-view #{{uiViewData?.id}} created by '{{ parentContext?.name || "(root)" }}' state</div>
// <div>name: (absolute) '{{uiViewData?.fqn}}' (contextual) '{{uiViewData?.name}}@{{parentContext?.name}}' </div>
// <div>currently filled by: '{{(uiViewData?.config && uiViewData?.config?.viewDecl?.$context) || 'empty...'}}'</div>
// </div>
//
// </div>`
})
export class UIView {
@ViewChild('componentTarget', {read: ViewContainerRef}) componentTarget: ViewContainerRef;
@ViewChild('componentTarget', {read: ViewContainerRef}) _componentTarget: ViewContainerRef;
@Input('name') name: string;
@Input('ui-view') set _name(val: string) { this.name = val; }
/** The reference to the component currently inside the viewport */
componentRef: ComponentRef<any>;
_componentRef: ComponentRef<any>;
/** Deregisters the ui-view from the view service */
deregisterUIView: Function;
private _deregisterUIView: Function;
/** Deregisters the master uiCanExit transition hook */
deregisterHook: Function;
private _deregisterHook: Function;
/** Data about the this UIView */
uiViewData: ActiveUIView = <any> {};
parent: ParentUIViewInject;
private _uiViewData: ActiveUIView = <any> {};
private _parent: ParentUIViewInject;

static PARENT_INJECT = "UIView.PARENT_INJECT";

Expand All @@ -129,26 +114,26 @@ export class UIView {
@Inject(UIView.PARENT_INJECT) parent,
public viewContainerRef: ViewContainerRef,
) {
this.parent = parent;
this._parent = parent;
}

ngOnInit() {
const router = this.router;
const parentFqn = this.parent.fqn;
const parentFqn = this._parent.fqn;
const name = this.name || '$default';

this.uiViewData = {
this._uiViewData = {
$type: 'ng2',
id: id++,
name: name,
fqn: parentFqn ? parentFqn + "." + name : name,
creationContext: this.parent.context,
configUpdated: this.viewConfigUpdated.bind(this),
creationContext: this._parent.context,
configUpdated: this._viewConfigUpdated.bind(this),
config: undefined
};

this.deregisterHook = router.transitionService.onBefore({}, trans => this.applyUiCanExitHook(trans));
this.deregisterUIView = router.viewService.registerUIView(this.uiViewData);
this._deregisterHook = router.transitionService.onBefore({}, trans => this._applyUiCanExitHook(trans));
this._deregisterUIView = router.viewService.registerUIView(this._uiViewData);
}

/**
Expand All @@ -159,12 +144,12 @@ export class UIView {
*
* If both are true, adds the uiCanExit component function as a hook to that singular Transition.
*/
applyUiCanExitHook(trans: Transition) {
const instance = this.componentRef && this.componentRef.instance;
private _applyUiCanExitHook(trans: Transition) {
const instance = this._componentRef && this._componentRef.instance;
const uiCanExitFn: TransitionHookFn = instance && instance.uiCanExit;

if (isFunction(uiCanExitFn)) {
const state: StateDeclaration = parse("uiViewData.config.viewDecl.$context.self")(this);
const state: StateDeclaration = parse("_uiViewData.config.viewDecl.$context.self")(this);

if (trans.exiting().indexOf(state) !== -1) {
trans.onStart({}, function() {
Expand All @@ -174,55 +159,58 @@ export class UIView {
}
}

disposeLast() {
if (this.componentRef) this.componentRef.destroy();
this.componentRef = null;
private _disposeLast() {
if (this._componentRef) this._componentRef.destroy();
this._componentRef = null;
}

ngOnDestroy() {
if (this.deregisterUIView) this.deregisterUIView();
if (this.deregisterHook) this.deregisterHook();
this.disposeLast();
if (this._deregisterUIView) this._deregisterUIView();
if (this._deregisterHook) this._deregisterHook();
this._disposeLast();
}

/**
* The view service is informing us of an updated ViewConfig
* (usually because a transition activated some state and its views)
*/
viewConfigUpdated(config: ViewConfig) {
_viewConfigUpdated(config: ViewConfig) {
// The config may be undefined if there is nothing currently targeting this UIView.
// Dispose the current component, if there is one
if (!config) return this.disposeLast();
if (!config) return this._disposeLast();

// Only care about Ng2 configs
if (!(config instanceof Ng2ViewConfig)) return;

// The "new" viewconfig is already applied, so exit early
if (this.uiViewData.config === config) return;
if (this._uiViewData.config === config) return;

// This is a new ViewConfig. Dispose the previous component
this.disposeLast();
trace.traceUIViewConfigUpdated(this.uiViewData, config && config.viewDecl.$context);
this._disposeLast();
trace.traceUIViewConfigUpdated(this._uiViewData, config && config.viewDecl.$context);

this.applyUpdatedConfig(config);
this._applyUpdatedConfig(config);
}

applyUpdatedConfig(config: Ng2ViewConfig) {
this.uiViewData.config = config;
private _applyUpdatedConfig(config: Ng2ViewConfig) {
this._uiViewData.config = config;
// Create the Injector for the routed component
let context = new ResolveContext(config.path);
let componentInjector = this.getComponentInjector(context);
let componentInjector = this._getComponentInjector(context);

// Get the component class from the view declaration. TODO: allow promises?
let componentClass = config.viewDecl.component;

// Create the component
let compFactoryResolver = componentInjector.get(ComponentFactoryResolver);
let compFactory = compFactoryResolver.resolveComponentFactory(componentClass);
this.componentRef = this.componentTarget.createComponent(compFactory, undefined, componentInjector);
this._componentRef = this._componentTarget.createComponent(compFactory, undefined, componentInjector);

// Wire resolves to @Input()s
this.applyInputBindings(compFactory, this.componentRef, context, componentClass);
this._applyInputBindings(compFactory, this._componentRef.instance, context, componentClass);

// Initiate change detection for the newly created component
this._componentRef.changeDetectorRef.markForCheck();
}

/**
Expand All @@ -235,12 +223,12 @@ export class UIView {
*
* @returns an Injector
*/
getComponentInjector(context: ResolveContext): Injector {
private _getComponentInjector(context: ResolveContext): Injector {
// Map resolves to "useValue: providers"
let resolvables = context.getTokens().map(token => context.getResolvable(token)).filter(r => r.resolved);
let newProviders = resolvables.map(r => ({ provide: r.token, useValue: r.data }));

let parentInject = { context: this.uiViewData.config.viewDecl.$context, fqn: this.uiViewData.fqn };
let parentInject = { context: this._uiViewData.config.viewDecl.$context, fqn: this._uiViewData.fqn };
newProviders.push({ provide: UIView.PARENT_INJECT, useValue: parentInject });

let parentComponentInjector = this.viewContainerRef.injector;
Expand All @@ -256,9 +244,8 @@ export class UIView {
* Finds component inputs which match resolves (by name) and sets the input value
* to the resolve data.
*/
applyInputBindings(factory: ComponentFactory<any>, ref: ComponentRef<any>, context: ResolveContext, componentClass) {
const component = ref.instance;
const bindings = this.uiViewData.config.viewDecl['bindings'] || {};
private _applyInputBindings(factory: ComponentFactory<any>, component: any, context: ResolveContext, componentClass) {
const bindings = this._uiViewData.config.viewDecl['bindings'] || {};
const explicitBoundProps = Object.keys(bindings);

// Returns the actual component property for a renamed an input renamed using `@Input('foo') _foo`.
Expand All @@ -285,8 +272,5 @@ export class UIView {
.map(addResolvable)
.filter(tuple => tuple.resolvable && tuple.resolvable.resolved)
.forEach(tuple => { component[tuple.prop] = tuple.resolvable.data; });

// Initiate change detection for the newly created component
ref.changeDetectorRef.detectChanges();
}
}

0 comments on commit 3d532b6

Please sign in to comment.