Skip to content

Commit

Permalink
Allow outlet of dialogs within a transition (#6)
Browse files Browse the repository at this point in the history
* allow outlet of dialogs with a transition

* tests

* abstract OutletBase

* implement IOutletComponent identifier concept

* TODO
  • Loading branch information
DavidVollmers authored Aug 29, 2023
1 parent af6757c commit 589fe64
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 62 deletions.
2 changes: 2 additions & 0 deletions packages/Ignis.Components/IOutletComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ namespace Ignis.Components;

public interface IOutletComponent : IComponent
{
object Identifier { get; }

RenderFragment? OutletContent { get; }

void SetOutlet(IOutlet outlet);
Expand Down
6 changes: 5 additions & 1 deletion packages/Ignis.Components/IgnisOutletComponentBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ public bool IgnoreOutlet
}
}

public RenderFragment OutletContent => BuildRenderTree;
public virtual object Identifier => this;

public virtual RenderFragment OutletContent => BuildRenderTree;

protected override bool ShouldRender => IgnoreOutlet || _outlet == null;

Expand All @@ -49,6 +51,8 @@ public void SetOutlet(IOutlet outlet)
if (_outlet != null && outlet != _outlet) throw new InvalidOperationException("Component is already adopted.");

_outlet = outlet;

//TODO rerender the original component to not have the same content rendered twice
}

public void SetFree()
Expand Down
80 changes: 80 additions & 0 deletions packages/Ignis.Components/OutletBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using Microsoft.AspNetCore.Components;

namespace Ignis.Components;

public abstract class OutletBase : IgnisComponentBase, IOutlet, IOutletRegistrySubscriber, IDisposable
{
private readonly IList<IOutletComponent> _components = new List<IOutletComponent>();

private IOutletRegistry? _outletRegistry;

protected IEnumerable<IOutletComponent> Components => _components;

[Inject]
public IOutletRegistry? OutletRegistry
{
get => _outletRegistry;
set
{
_outletRegistry?.Unsubscribe(this);

_outletRegistry = value;
_outletRegistry?.Subscribe(this);
}
}

/// <inheritdoc />
public virtual void OnComponentRegistered(IOutletComponent component)
{
if (_components.Contains(component) || _components.Any(c => c.Identifier.Equals(component.Identifier))) return;

_components.Add(component);

component.SetOutlet(this);

base.Update();
}

/// <inheritdoc />
public void OnComponentUnregistered(IOutletComponent component)
{
if (!_components.Contains(component)) return;

_components.Remove(component);

component.SetFree();

base.Update();
}

void IOutlet.Update(bool async)
{
base.Update(async);
}

protected virtual void Dispose(bool disposing)
{
if (!disposing) return;

_outletRegistry?.Unsubscribe(this);
_outletRegistry = null;

foreach (var component in _components)
{
component.SetFree();
}

_components.Clear();
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

~OutletBase()
{
Dispose(false);
}
}
9 changes: 8 additions & 1 deletion packages/Tailwind/Ignis.Components.HeadlessUI/Dialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ public bool IsOpen
/// <inheritdoc />
public IEnumerable<KeyValuePair<string, object?>> Attributes => _attributes;

/// <inheritdoc />
public override RenderFragment OutletContent => Transition?.RenderFragment ?? base.OutletContent;

/// <inheritdoc />
public override object Identifier => Transition ?? (object)this;

[Inject] internal FrameTracker FrameTracker { get; set; } = null!;

public Dialog()

Check warning on line 95 in packages/Tailwind/Ignis.Components.HeadlessUI/Dialog.cs

View workflow job for this annotation

GitHub Actions / Test

Missing XML comment for publicly visible type or member 'Dialog.Dialog()'

Check warning on line 95 in packages/Tailwind/Ignis.Components.HeadlessUI/Dialog.cs

View workflow job for this annotation

GitHub Actions / Pack

Missing XML comment for publicly visible type or member 'Dialog.Dialog()'
Expand All @@ -92,7 +98,8 @@ public Dialog()

_attributes = new AttributeCollection(new[]
{
() => new KeyValuePair<string, object?>("id", Id), () => new KeyValuePair<string, object?>("role", "dialog"),
() => new KeyValuePair<string, object?>("id", Id),
() => new KeyValuePair<string, object?>("role", "dialog"),
() => new KeyValuePair<string, object?>("aria-modal", "true"), () => new KeyValuePair<string, object?>(
"aria-labelledby", Title == null ? null : Title.Id ?? Id + "-title"),
() => new KeyValuePair<string, object?>("aria-describedby",
Expand Down
70 changes: 10 additions & 60 deletions packages/Tailwind/Ignis.Components.HeadlessUI/DialogOutlet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@

namespace Ignis.Components.HeadlessUI;

public sealed class DialogOutlet : IgnisComponentBase, IDynamicComponent, IOutletRegistrySubscriber, IOutlet,
IDisposable
public sealed class DialogOutlet : OutletBase, IDynamicComponent
{
private readonly IList<IDialog> _dialogs = new List<IDialog>();

private IOutletRegistry? _outletRegistry;
private Type? _asComponent;
private string? _asElement;

Expand Down Expand Up @@ -36,18 +32,8 @@ public Type? AsComponent
}
}

[Inject]
public IOutletRegistry? OutletRegistry
{
get => _outletRegistry;
set
{
_outletRegistry?.Unsubscribe(this);

_outletRegistry = value;
_outletRegistry?.Subscribe(this);
}
}
[Parameter(CaptureUnmatchedValues = true)]
public IEnumerable<KeyValuePair<string, object?>>? AdditionalAttributes { get; set; }

/// <inheritdoc cref="IDynamicParentComponent{T}.Element" />
public ElementReference? Element { get; set; }
Expand All @@ -64,60 +50,24 @@ public DialogOutlet()
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenAs(0, this);
builder.AddMultipleAttributes(1, AdditionalAttributes!);
// ReSharper disable once VariableHidesOuterVariable
builder.AddContentFor(1, this, builder =>
builder.AddContentFor(2, this, builder =>
{
foreach (var dialog in _dialogs)
foreach (var dialog in Components)
{
builder.AddContent(2, dialog.OutletContent);
builder.AddContent(3, dialog.OutletContent);
}
});

builder.CloseAs(this);
}

/// <inheritdoc />
public void OnComponentRegistered(IOutletComponent component)
{
if (component is not IDialog dialog) return;

_dialogs.Add(dialog);

dialog.SetOutlet(this);

base.Update();
}

/// <inheritdoc />
public void OnComponentUnregistered(IOutletComponent component)
public override void OnComponentRegistered(IOutletComponent component)
{
if (component is not IDialog dialog) return;

if (!_dialogs.Contains(dialog)) return;

_dialogs.Remove(dialog);

dialog.SetFree();

base.Update();
}

/// <inheritdoc />
public new void Update(bool async = false)
{
base.Update(async);
}

public void Dispose()
{
_outletRegistry?.Unsubscribe(this);
_outletRegistry = null;

foreach (var dialog in _dialogs)
{
dialog.SetFree();
}
if (component is not IDialog) return;

_dialogs.Clear();
base.OnComponentRegistered(component);
}
}
4 changes: 4 additions & 0 deletions packages/Tailwind/Ignis.Components.HeadlessUI/ITransition.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
using Ignis.Components.Web;
using Microsoft.AspNetCore.Components;

namespace Ignis.Components.HeadlessUI;

public interface ITransition : IDynamicParentComponent<ITransition>, ICssClass
{
// required for outlet components to render within a transition (e.g. Dialog)
internal RenderFragment RenderFragment { get; }

void Hide(Action? continueWith = null);

void Show(Action? continueWith = null);
Expand Down
3 changes: 3 additions & 0 deletions packages/Tailwind/Ignis.Components.HeadlessUI/Transition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ public bool Show
/// <inheritdoc />
public object? Component { get; set; }

/// <inheritdoc />
public RenderFragment RenderFragment => BuildRenderTree;

public Transition()
{
AsElement = "div";
Expand Down
72 changes: 72 additions & 0 deletions tests/Ignis.Tests.Components.HeadlessUI/DialogTests.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
@inherits TestContext

@code
{
[Fact]
public void Outlet()
{
Services.AddIgnis();
Services.AddSingleton<IHostContext, TestHostContext>();

JSInterop.Mode = JSRuntimeMode.Loose;

const string dialogId = "dialog";
const string outletId = "dialog-outlet";

var cut = Render(@<div>
<DialogOutlet AsElement="div" id="@outletId"/>
<Dialog IsOpen id="@dialogId"></Dialog>
</div>);

var dialogDiv = cut.Find($"#{dialogId}");
var outletDiv = cut.Find($"#{outletId}");
Assert.True(outletDiv.Contains(dialogDiv));
}

[Fact]
public void IgnoreOutlet()
{
Services.AddIgnis();
Services.AddSingleton<IHostContext, TestHostContext>();

JSInterop.Mode = JSRuntimeMode.Loose;

const string dialogId = "dialog";
const string outletId = "dialog-outlet";

var cut = Render(@<div>
<DialogOutlet AsElement="div" id="@outletId"/>
<Dialog IgnoreOutlet IsOpen id="@dialogId"></Dialog>
</div>);

var dialogDiv = cut.Find($"#{dialogId}");
var outletDiv = cut.Find($"#{outletId}");
Assert.False(outletDiv.Contains(dialogDiv));
}

[Fact]
public void OutletWithTransition()
{
Services.AddIgnis();
Services.AddSingleton<IHostContext, TestHostContext>();

JSInterop.Mode = JSRuntimeMode.Loose;

const string dialogId = "dialog";
const string outletId = "dialog-outlet";
const string transitionId = "dialog-transition";

var cut = Render(@<div>
<DialogOutlet AsElement="div" id="@outletId"/>
<Transition Show id="@transitionId">
<Dialog IsOpen id="@dialogId"></Dialog>
</Transition>
</div>);

var transitionDiv = cut.Find($"#{transitionId}");
var dialogDiv = cut.Find($"#{dialogId}");
var outletDiv = cut.Find($"#{outletId}");
Assert.True(outletDiv.Contains(transitionDiv));
Assert.True(transitionDiv.Contains(dialogDiv));
}
}

0 comments on commit 589fe64

Please sign in to comment.