├── .github └── workflows │ ├── demo.yml │ └── nuget.yml ├── .gitignore ├── BlazorHooked ├── BlazorHooked.csproj ├── Connect.razor ├── ConnectStore.razor ├── Hook.razor ├── HookComponentBase.razor ├── HookContext.Effects.cs ├── HookContext.State.cs ├── HookContext.cs ├── Loader.razor ├── LoaderReducer.cs ├── ReducerMap.cs ├── SagaRegistration.cs └── _Imports.razor ├── Demo ├── App.razor ├── Demo.csproj ├── Pages │ ├── GlobalStore │ │ ├── BasicExample.snip.razor │ │ ├── GlobalStore.razor │ │ └── WithSagas.snip.razor │ ├── Helpers │ │ ├── Helpers.razor │ │ └── Loader.snip.razor │ ├── Hooks │ │ ├── Hooks.razor │ │ ├── UseEffect.snip.razor │ │ ├── UseReducer.snip.razor │ │ ├── UseState.snip.razor │ │ └── WithSaga.snip.razor │ └── Index │ │ ├── BaseComponent.snip.razor │ │ ├── GlobalStore.snip.razor │ │ ├── HookComponent.snip.razor │ │ ├── Index.razor │ │ └── MultipleHooks.snip.razor ├── Program.cs ├── Properties │ └── launchSettings.json ├── Shared │ ├── Code.razor │ ├── CodeDemo.razor │ ├── MainLayout.razor │ └── MainLayout.razor.css ├── _Imports.razor ├── libman.json └── wwwroot │ ├── css │ └── app.css │ ├── index.html │ ├── js │ └── app.js │ ├── lib │ └── prism │ │ ├── prism.min.js │ │ └── themes │ │ └── prism.min.css │ ├── logo.ico │ ├── logo.png │ └── logo.svg ├── LICENSE ├── README.md ├── assets ├── logo.ico ├── logo.png └── logo.svg └── blazor-hooked.sln /.github/workflows/demo.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [main] 7 | tags: "*" 8 | pull_request: 9 | branches: [main] 10 | 11 | jobs: 12 | deploy-to-github-pages: 13 | runs-on: ubuntu-latest 14 | defaults: 15 | run: 16 | working-directory: Demo 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Setup .NET Core SDK 20 | uses: actions/setup-dotnet@v1 21 | with: 22 | dotnet-version: 6.0.x 23 | - name: Restore dependencies 24 | run: dotnet restore 25 | - name: Publish .NET Core Project 26 | run: dotnet publish Demo.csproj -c Release -o publish --nologo --no-restore 27 | - name: Update for GH Pages 28 | run: | 29 | sed -i 's///g' publish/wwwroot/index.html 30 | cp publish/wwwroot/index.html publish/wwwroot/404.html 31 | touch publish/wwwroot/.nojekyll 32 | - name: Publish to GitHub Pages 33 | uses: JamesIves/github-pages-deploy-action@v4.2.2 34 | if: ${{ github.ref == 'refs/heads/main' }} 35 | with: 36 | branch: gh-pages 37 | folder: Demo/publish/wwwroot 38 | -------------------------------------------------------------------------------- /.github/workflows/nuget.yml: -------------------------------------------------------------------------------- 1 | name: Nuget 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [main] 7 | tags: "*" 8 | paths: BlazorHooked/** 9 | pull_request: 10 | branches: [main] 11 | paths: BlazorHooked/** 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | defaults: 17 | run: 18 | working-directory: BlazorHooked 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Get the version 22 | id: tag 23 | run: | 24 | echo "ref: $GITHUB_REF" 25 | echo "tag: ${GITHUB_REF/refs\/tags\//}" 26 | version=$([[ "$GITHUB_REF" == *"refs/tags/"* ]] && echo ${GITHUB_REF/refs\/tags\//} || echo "1.0.0") 27 | echo $version 28 | echo ::set-output name=VERSION::$version 29 | - name: Setup .NET 30 | uses: actions/setup-dotnet@v1 31 | with: 32 | dotnet-version: 6.0.x 33 | - name: Restore dependencies 34 | run: dotnet restore 35 | - name: Pack 36 | run: dotnet pack --no-restore -c release --include-symbols /p:SymbolPackageFormat=snupkg /p:AssemblyVersion=1.0.0.0 /p:Version=${{ steps.tag.outputs.VERSION }} 37 | - name: Push 38 | if: github.ref_type == 'tag' 39 | env: 40 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} 41 | VERSION: ${{ steps.tag.outputs.VERSION }} 42 | run: dotnet nuget push "**/BlazorHooked.$VERSION.nupkg" -s https://api.nuget.org/v3/index.json -k "$NUGET_API_KEY" 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /BlazorHooked/BlazorHooked.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | BlazorHooked 5 | logo.png 6 | James Dibble 7 | https://github.com/dibble-james/blazor-hooked 8 | blazor redux flux hooks state-management 9 | https://dibble-james.github.io/blazor-hooked/ 10 | README.md 11 | LICENSE 12 | A minimal boiler-plate, state management framework for Blazor that resembles React Hooks. 13 | net6.0 14 | enable 15 | enable 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /BlazorHooked/Connect.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Components.Rendering 2 | @attribute [CascadingTypeParameter(nameof(TState))] 3 | @typeparam TProps where TProps : new() 4 | @typeparam TState 5 | 6 | @code { 7 | public delegate Action> Connector(Func mapStateToProps, Func? mapDispatchToProps); 8 | 9 | [Parameter, EditorRequired] 10 | public RenderFragment ChildContent { get; init; } = null!; 11 | 12 | [CascadingParameter] 13 | private TState Store { get; init; } = default!; 14 | 15 | [CascadingParameter] 16 | private Dispatch Dispatch { get; init; } = null!; 17 | 18 | public RenderFragment Map() => (builder) => 19 | { 20 | ChildContent((Func mapStateToContext, Func? mapDispatchToProps) => 21 | { 22 | var props = mapStateToContext(Store); 23 | 24 | if (mapDispatchToProps is not null) 25 | { 26 | props = mapDispatchToProps(props, (action) => () => Dispatch(action)); 27 | } 28 | 29 | return children => children(props)(builder); 30 | })(builder); 31 | }; 32 | } 33 | 34 | @Map() -------------------------------------------------------------------------------- /BlazorHooked/ConnectStore.razor: -------------------------------------------------------------------------------- 1 | @inherits HookComponentBase 2 | @typeparam TState 3 | 4 | @code { 5 | private static readonly Reducer EmptyReducer = (state, _) => state; 6 | 7 | [Parameter, EditorRequired] 8 | public Func, Reducer> RootReducer { get; init; } = null!; 9 | 10 | [Parameter] 11 | public Func<(TState?, Dispatch), (TState?, Dispatch)>? ConfigureDispatch { get; init; } 12 | 13 | [Parameter, EditorRequired] 14 | public Func InitialStateFactory { get; init; } = null!; 15 | 16 | [Parameter, EditorRequired] 17 | public RenderFragment ChildContent { get; init; } = null!; 18 | } 19 | 20 | @{ 21 | var (state, dispatch) = Hook.UseReducer(RootReducer!(EmptyReducer), InitialStateFactory!()); 22 | 23 | if (ConfigureDispatch is not null) 24 | { 25 | (state, dispatch) = ConfigureDispatch((state, dispatch)); 26 | } 27 | 28 | 29 | 30 | @ChildContent 31 | 32 | 33 | } -------------------------------------------------------------------------------- /BlazorHooked/Hook.razor: -------------------------------------------------------------------------------- 1 | @inherits HookComponentBase 2 | 3 | @code { 4 | [Parameter, EditorRequired] 5 | public RenderFragment? ChildContent { get; init; } 6 | } 7 | 8 | @ChildContent!(this.Hook) -------------------------------------------------------------------------------- /BlazorHooked/HookComponentBase.razor: -------------------------------------------------------------------------------- 1 | @code { 2 | public HookComponentBase() 3 | { 4 | this.Hook = new HookContext(); 5 | this.Hook.StateHasChanged += OnStateChanged; 6 | } 7 | 8 | protected HookContext Hook { get; } 9 | 10 | protected override async Task OnParametersSetAsync() 11 | { 12 | await base.OnParametersSetAsync(); 13 | 14 | await this.Hook.RunEffects(); 15 | } 16 | 17 | protected override async Task OnAfterRenderAsync(bool firstRender) 18 | { 19 | await base.OnAfterRenderAsync(firstRender); 20 | 21 | await this.Hook.RunEffects(); 22 | } 23 | 24 | public ValueTask DisposeAsync() 25 | { 26 | this.Hook.StateHasChanged -= OnStateChanged; 27 | return ((IAsyncDisposable)this.Hook).DisposeAsync(); 28 | } 29 | 30 | private void OnStateChanged() => this.InvokeAsync(StateHasChanged); 31 | } 32 | -------------------------------------------------------------------------------- /BlazorHooked/HookContext.Effects.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorHooked; 2 | 3 | using OneOf; 4 | using System.Collections.Concurrent; 5 | using System.Runtime.CompilerServices; 6 | 7 | public delegate void Effect(); 8 | 9 | public delegate Task AsyncEffect(); 10 | 11 | public delegate Action CallbackEffect(); 12 | 13 | public delegate Task> AsyncCallbackEffect(); 14 | 15 | public partial class HookContext : IAsyncDisposable 16 | { 17 | private readonly ConcurrentDictionary effects = new(); 18 | private readonly ConcurrentQueue> effectQueue = new(); 19 | private readonly ConcurrentDictionary>> effectCallbacks = new(); 20 | 21 | public void UseEffect(Effect effect, [CallerLineNumber] int caller = 0) 22 | => this.UseEffectInternal(effect, Array.Empty(), caller); 23 | 24 | public void UseEffect(Effect effect, object[] dependencies, [CallerLineNumber] int caller = 0) 25 | => this.UseEffectInternal(effect, dependencies, caller); 26 | 27 | public void UseEffect(AsyncEffect effect, [CallerLineNumber] int caller = 0) 28 | => this.UseEffectInternal(effect, Array.Empty(), caller); 29 | 30 | public void UseEffect(AsyncEffect effect, object[] dependencies, [CallerLineNumber] int caller = 0) 31 | => this.UseEffectInternal(effect, dependencies, caller); 32 | 33 | public void UseEffect(CallbackEffect effect, [CallerLineNumber] int caller = 0) 34 | => this.UseEffectInternal(effect, Array.Empty(), caller); 35 | 36 | public void UseEffect(CallbackEffect effect, object[] dependencies, [CallerLineNumber] int caller = 0) 37 | => this.UseEffectInternal(effect, dependencies, caller); 38 | 39 | public void UseEffect(AsyncCallbackEffect effect, [CallerLineNumber] int caller = 0) 40 | => this.UseEffectInternal(effect, Array.Empty(), caller); 41 | 42 | public void UseEffect(AsyncCallbackEffect effect, object[] dependencies, [CallerLineNumber] int caller = 0) 43 | => this.UseEffectInternal(effect, dependencies, caller); 44 | 45 | private void UseEffectInternal(OneOf effect, object[] dependecies, int caller) 46 | { 47 | if (this.effects.TryAdd(caller, dependecies)) 48 | { 49 | this.effectQueue.Enqueue(RunEffect(caller, effect)); 50 | return; 51 | } 52 | 53 | if (this.effects.TryGetValue(caller, out var registeredDependencies)) 54 | { 55 | // The dependencies have not been updated so no-op 56 | if (dependecies.Length == registeredDependencies.Length && dependecies.Zip(registeredDependencies, (a, b) => a.Equals(b)).All(x => x)) 57 | { 58 | return; 59 | } 60 | 61 | if (this.effects.TryUpdate(caller, dependecies, registeredDependencies)) 62 | { 63 | if (this.effectCallbacks.TryRemove(caller, out var callback)) 64 | { 65 | this.effectQueue.Enqueue(RunCallback(callback)); 66 | } 67 | 68 | this.effectQueue.Enqueue(RunEffect(caller, effect)); 69 | } 70 | } 71 | } 72 | 73 | internal async Task RunEffects() 74 | { 75 | while (this.effectQueue.TryDequeue(out var effect)) 76 | { 77 | await effect(); 78 | } 79 | } 80 | 81 | private Func RunEffect(int caller, OneOf effect) => () => 82 | effect.Match(e => 83 | { 84 | e(); 85 | this.effectCallbacks.TryRemove(caller, out var _); 86 | return Task.CompletedTask; 87 | }, 88 | async e => 89 | { 90 | await e(); 91 | this.effectCallbacks.TryRemove(caller, out var _); 92 | }, 93 | e => 94 | { 95 | this.effectCallbacks.AddOrUpdate(caller, e(), (_, _) => e()); 96 | return Task.CompletedTask; 97 | }, 98 | async e => 99 | { 100 | var cb = await e(); 101 | this.effectCallbacks.AddOrUpdate(caller, cb, (_, _) => cb); 102 | }); 103 | 104 | private Func RunCallback(OneOf> callback) => () => 105 | callback.Match(e => 106 | { 107 | e(); 108 | return Task.CompletedTask; 109 | }, 110 | func => func()); 111 | 112 | public async ValueTask DisposeAsync() 113 | { 114 | foreach (var callback in this.effectCallbacks.Values) 115 | { 116 | await callback.Match( 117 | cb => 118 | { 119 | cb(); 120 | return Task.CompletedTask; 121 | }, 122 | cb => cb()); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /BlazorHooked/HookContext.State.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorHooked; 2 | 3 | using System.Collections.Concurrent; 4 | using System.Runtime.CompilerServices; 5 | 6 | public delegate void SetState(T newValue); 7 | 8 | public delegate void Dispatch(object action); 9 | 10 | public delegate T Reducer(T state, object action); 11 | 12 | public partial class HookContext : IAsyncDisposable 13 | { 14 | private readonly ConcurrentDictionary states = new(); 15 | 16 | public (T? state, SetState setState) UseState(T initialValue, [CallerLineNumber] int caller = 0) 17 | => ((T?)this.states.GetOrAdd(caller, initialValue), this.SetState(caller)); 18 | 19 | public (T? state, Dispatch dispatch) UseReducer(Reducer reducer, T initialState, [CallerLineNumber] int caller = 0) 20 | => ((T?)this.states.GetOrAdd(caller, initialState), this.Dispatch(caller, reducer)); 21 | 22 | private SetState SetState(int index) => newState => 23 | { 24 | this.states[index] = newState; 25 | this.StateHasChanged?.Invoke(); 26 | }; 27 | 28 | private Dispatch Dispatch(int index, Reducer reducer) => action => 29 | { 30 | this.states[index] = reducer((T?)this.states[index]!, action); 31 | this.StateHasChanged?.Invoke(); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /BlazorHooked/HookContext.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorHooked; 2 | 3 | public partial class HookContext 4 | { 5 | public event Action? StateHasChanged; 6 | } 7 | -------------------------------------------------------------------------------- /BlazorHooked/Loader.razor: -------------------------------------------------------------------------------- 1 | @typeparam T 2 | 3 | @code { 4 | [Parameter, EditorRequired] 5 | public Func>? Load { get; init; } 6 | 7 | [Parameter] 8 | public RenderFragment? Loading { get; init; } 9 | 10 | [Parameter, EditorRequired] 11 | public RenderFragment? Loaded { get; init; } 12 | 13 | [Parameter] 14 | public RenderFragment? Failed { get; init; } 15 | 16 | [Parameter] 17 | public object[]? Dependencies { get; init; } 18 | } 19 | 20 | 21 | @{ 22 | var (state, dispatch) = context.UseLoaderReducer(this.Load!); 23 | context.UseEffect(() => dispatch(new LoaderActions.Load()), Dependencies ?? Array.Empty()); 24 | 25 | if (state!.Loading && this.Loading is not null) 26 | { 27 | @Loading!(true) 28 | } 29 | 30 | if (state!.TryGetResult(out var result)) 31 | { 32 | @Loaded!(result) 33 | } 34 | 35 | if (state!.TryGetFailure(out var failure) && this.Failed is not null) 36 | { 37 | @Failed!(failure) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /BlazorHooked/LoaderReducer.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorHooked; 2 | 3 | using System.Runtime.CompilerServices; 4 | 5 | public static class LoaderReducer 6 | { 7 | public static (LoaderState? state, Dispatch dispatch) UseLoaderReducer(this HookContext context, Func> loader, [CallerLineNumber] int caller = 0) 8 | => context.UseReducer(Reducer, new LoaderState(false, false, default, null), caller).Subscribe(Saga(loader)); 9 | 10 | private static LoaderState Reducer(LoaderState state, object action) => action switch 11 | { 12 | LoaderActions.Load => state with { Loading = true, Result = default, Failure = null, Loaded = false }, 13 | LoaderActions.Loaded loaded => state with { Loading = false, Loaded = true, Result = loaded.Result }, 14 | LoaderActions.Failed failed => state with { Loading = false, Failure = failed.Failure }, 15 | _ => state 16 | }; 17 | 18 | private static Saga> Saga(Func> loader) => async (action, dispatch) => 19 | { 20 | try 21 | { 22 | var result = await loader(); 23 | 24 | dispatch(new LoaderActions.Loaded(result)); 25 | } 26 | catch (Exception ex) 27 | { 28 | dispatch(new LoaderActions.Failed(ex)); 29 | } 30 | }; 31 | } 32 | 33 | public static class LoaderActions 34 | { 35 | public record Load(); 36 | 37 | public record Loaded(T Result); 38 | 39 | public record Failed(Exception Failure); 40 | } 41 | 42 | public record LoaderState(bool Loading, bool Loaded, T? Result, Exception? Failure) 43 | { 44 | public bool TryGetResult(out T? result) 45 | { 46 | result = Result; 47 | 48 | return Loaded; 49 | } 50 | 51 | public bool TryGetFailure(out Exception? failure) 52 | { 53 | failure = default; 54 | 55 | if (Loading || Result is not null) 56 | { 57 | return false; 58 | } 59 | 60 | failure = Failure; 61 | return true; 62 | } 63 | } -------------------------------------------------------------------------------- /BlazorHooked/ReducerMap.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorHooked; 2 | 3 | public delegate Action DispatchAction(object action); 4 | 5 | public delegate T ReducerExecutor(T state); 6 | 7 | public delegate T ReducerCombiner(T state, ReducerExecutor reducer); 8 | 9 | public static class CombinedReducer 10 | { 11 | public static Reducer CombineReducer( 12 | this Reducer parentReducer, ReducerCombiner map, Reducer reducer) => (state, action) => 13 | map(parentReducer(state, action), subState => reducer(subState, action)); 14 | } 15 | -------------------------------------------------------------------------------- /BlazorHooked/SagaRegistration.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorHooked; 2 | 3 | public delegate Task Saga(TTrigger action, Dispatch dispatch); 4 | 5 | public static class SagaRegistration 6 | { 7 | public static (T? state, Dispatch dispatch) Subscribe(this (T? State, Dispatch Dispatch) reducer, Saga saga) 8 | => (reducer.State, WrapDispatch(reducer.Dispatch, saga)); 9 | 10 | private static Dispatch WrapDispatch(Dispatch dispatch, Saga saga) => action => 11 | { 12 | dispatch(action); 13 | 14 | if (action is TTrigger trigger) 15 | { 16 | Task.Run(async () => await saga(trigger, dispatch)); 17 | } 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /BlazorHooked/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Components.Web 2 | -------------------------------------------------------------------------------- /Demo/App.razor: -------------------------------------------------------------------------------- 1 | @code { 2 | public record AppState(); 3 | 4 | private Reducer RootReducer(Reducer reducer) => reducer; 5 | } 6 | 7 | new AppState())> 8 | 9 | 10 | 11 | 12 | 13 | 14 | Not found 15 | 16 |

Sorry, there's nothing at this address.

17 |
18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /Demo/Demo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Always 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Demo/Pages/GlobalStore/BasicExample.snip.razor: -------------------------------------------------------------------------------- 1 | @using BlazorHooked 2 | 3 | @code { 4 | public record UserState(string? Username, string? Email, bool LoggedIn); 5 | 6 | public record State 7 | { 8 | public UserState User { get; init; } = new UserState(null, null, false); 9 | } 10 | 11 | private record Login(); 12 | private record Logout(); 13 | 14 | private Reducer UserReducer() => (s, a) => a switch 15 | { 16 | Login l => s with { LoggedIn = true, Username = "John", Email = "John@test.com" }, 17 | Logout => s with { LoggedIn = false, Email = null, Username = null }, 18 | _ => s, 19 | }; 20 | 21 | private Reducer RootReducer(Reducer reducer) 22 | => reducer.CombineReducer((s, r) => s with { User = r(s.User) }, UserReducer()); 23 | 24 | private record Props 25 | { 26 | public UserState User { get; init; } = null!; 27 | 28 | public Action ButtonAction { get; init; } = null!; 29 | } 30 | 31 | private Props MapDispatchToProps(Props context, DispatchAction d) => context with 32 | { 33 | ButtonAction = context.User.LoggedIn ? d(new Logout()) : d(new Login()), 34 | }; 35 | } 36 | 37 | new State())> 38 |

Outside the store

39 | 40 |

In the store

41 | @{ 42 | connect(s => new Props { User = s.User }, MapDispatchToProps)(props => 43 | { 44 | return @
45 |
46 |
Username
47 |
@props.User?.Username
48 |
Email
49 |
@props.User?.Email
50 |
51 | 54 |
; 55 | }); 56 | } 57 |
58 |
59 | -------------------------------------------------------------------------------- /Demo/Pages/GlobalStore/GlobalStore.razor: -------------------------------------------------------------------------------- 1 | @page "/store" 2 | 3 |

Store

4 |

5 | Components are really good for self-containing state. Most of the time, hooks are all you should 6 | need, but sometimes you need state to be available at multiple points of the render tree. Something 7 | like the current logged-in user: 8 |

9 | 10 |

11 | Underneath, the global store is just a UseReducer hook, but the state and dispatcher 12 | are available as CascadingParameters which are collected by the Connect component 13 | for you to hook into particular elements of the store. Reducers are exactly the same, but need to 14 | be registered to the global state by combining reducers for each part of the store into a single reducer. 15 |

16 |

17 | This is why you want to use a global store sparingly and why a lot of flux apps are hard to manage; the 18 | store just gets massive with hundreds of reducers and actions if every component has it’s own 19 | little portion. 20 |

21 |

22 | As we said earlier, the global store is just UseReducer so you can add dispatch middleware 23 | like subscriptions and sagas too. You add this via the ConfigureDispatch parameter. 24 |

25 | -------------------------------------------------------------------------------- /Demo/Pages/GlobalStore/WithSagas.snip.razor: -------------------------------------------------------------------------------- 1 | @using BlazorHooked 2 | 3 | @code { 4 | public record UserState(string? Username, string? Email, bool LoggedIn); 5 | 6 | public record State 7 | { 8 | public UserState User { get; init; } = new UserState(null, null, false); 9 | 10 | public DateTime LastLogin { get; init; } 11 | } 12 | 13 | private record Login(); 14 | private record Logout(); 15 | private record LastLogin(DateTime When); 16 | 17 | private Reducer UserReducer() => (s, a) => a switch 18 | { 19 | Login l => s with { LoggedIn = true, Username = "John", Email = "John@test.com" }, 20 | Logout => s with { LoggedIn = false, Email = null, Username = null }, 21 | _ => s, 22 | }; 23 | 24 | private Reducer LastLoginReducer() => (s, a) => a switch 25 | { 26 | LastLogin l => s with { LastLogin = l.When }, 27 | _ => s, 28 | }; 29 | 30 | private Reducer RootReducer(Reducer reducer) 31 | => reducer.CombineReducer((s, r) => s with { User = r(s.User) }, UserReducer()) 32 | .CombineReducer((s, r) => r(s), LastLoginReducer()); 33 | 34 | private record Props 35 | { 36 | public UserState User { get; init; } = null!; 37 | 38 | public Action ButtonAction { get; init; } = null!; 39 | 40 | public DateTime LastLogin { get; init; } 41 | } 42 | 43 | private Props MapDispatchToProps(Props context, DispatchAction d) => context with 44 | { 45 | ButtonAction = context.User.LoggedIn ? d(new Logout()) : d(new Login()), 46 | }; 47 | 48 | private (State, Dispatch) ConfigureDispatch(ValueTuple reducer) 49 | { 50 | return reducer.Subscribe((action, dispatch) => Task.Run(() => dispatch(new LastLogin(DateTime.Now)))); 51 | } 52 | } 53 | 54 | new State()) ConfigureDispatch=@ConfigureDispatch> 55 | 56 | @{ 57 | connect(s => new Props { User = s.User, LastLogin = s.LastLogin }, MapDispatchToProps)(props => 58 | { 59 | return @
60 |
61 |
Username
62 |
@props.User?.Username
63 |
Email
64 |
@props.User?.Email
65 |
Last Login
66 |
@props.LastLogin
67 |
68 | 71 |
; 72 | }); 73 | } 74 |
75 |
76 | -------------------------------------------------------------------------------- /Demo/Pages/Helpers/Helpers.razor: -------------------------------------------------------------------------------- 1 | @page "/helpers" 2 | 3 | Blazor Hooked - Helpers 4 | 5 |

Loader

6 | 7 |

A common use for a reducer is to track an async request. In-fact is so common that to save you some more boiler plate, the Loader component is built in.

8 | -------------------------------------------------------------------------------- /Demo/Pages/Helpers/Loader.snip.razor: -------------------------------------------------------------------------------- 1 | @using BlazorHooked 2 | 3 | @{ 4 | var (date, setDate) = Hook.UseState(0); 5 | } 6 | 7 |
8 | 9 | 10 |
11 | 12 | 13 | 14 |

Loading...

15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | @foreach (var forecast in forecasts) 28 | { 29 | 30 | 31 | 32 | 33 | 34 | 35 | } 36 | 37 |
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
38 |
39 | 40 |

Uhoh...

41 |
42 |
43 |
44 | 45 | @code { 46 | private Func> Loader(int date) 47 | => () => new WeatherForecastService().GetForecastAsync(date); 48 | 49 | public class WeatherForecast 50 | { 51 | public DateTime Date { get; set; } 52 | 53 | public int TemperatureC { get; set; } 54 | 55 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 56 | 57 | public string Summary { get; set; } = string.Empty; 58 | } 59 | 60 | public class WeatherForecastService 61 | { 62 | private static readonly string[] Summaries = new[] 63 | { 64 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 65 | }; 66 | 67 | public async Task GetForecastAsync(int offset) 68 | { 69 | await Task.Delay(500); 70 | 71 | if (Random.Shared.Next(1, 9) % 3 == 0) 72 | { 73 | throw new Exception("Failed"); 74 | } 75 | 76 | var startDate = DateTime.Now.AddDays(offset); 77 | 78 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 79 | { 80 | Date = startDate.AddDays(index), 81 | TemperatureC = Random.Shared.Next(-20, 55), 82 | Summary = Summaries[Random.Shared.Next(Summaries.Length)] 83 | }).ToArray(); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /Demo/Pages/Hooks/Hooks.razor: -------------------------------------------------------------------------------- 1 | @page "/hooks" 2 | Blazor Hooked - Hooks 3 | 4 |

Hooks

5 |

6 | BlazorHooked provides equivilants for the basic hooks that React provides. UseContext is mostly already provided by 7 | CascadingParameters. 8 |

9 |

UseState

10 |

11 | UseState takes an initial value for the state and returns a tuple, the first item being the current 12 | state, and the second being a function to update the state AKA the set function. You should treat the 13 | state as immutable and only update it via the set function. 14 |

15 |

16 | You would typically only really use UseState for small value types. The most simple usecase would be a 17 | counter: 18 |

19 | 20 | 21 |

UseReducer

22 |

23 | This is yet another Flux like thing, but Hooks make it so much simpler. It acts much like UseState but 24 | can handle more granular updates to the state via Actions. Like UseState it takes an initial state but 25 | also a Reducer. Again like UseState it returns the current state, but the set function is 26 | replaced by a Dispatcher. 27 |

28 |

Lets define some of those words:

29 |
    30 |
  • 31 | Action: A command or event to inform the reducer that the state should be changed. These are 32 | usually best served by records. 33 |
  • 34 |
  • 35 | Reducer: A function that takes the current state and an action and returns the new state. The 36 | framework doesn’t care how you do the reduction; it could be a local function, a static method on a seperate 37 | class or a type that you inject. It’s best practice, however, to not trigger things like http requests from the 38 | reducer, that’s what Effects are for. 39 |
  • 40 |
  • 41 | Dispatcher: A function provided by the framework that you call with an Action to invoke the 42 | Reducer. 43 |
  • 44 |
45 |

Lets refactor our counter:

46 | 47 |

48 | It’s a slightly trivial example but it shows the concepts. Checkout the Loader helper 49 | when you want to do async requests with a loading indicator. 50 |

51 | 52 |

UseEffect

53 |

54 | Effects are used to start background tasks and clean up after them when they’re finished with. The classic example 55 | would be to start listening on a websocket when the component is first rendered, then gracefully shutdown the 56 | connection when the component is unmounted and disposed. If your Effect uses a variable like a value from 57 | UseState or a component paramenter and you’d like the Effect to re-run when that changes, you add that 58 | variable as a Dependency by letting UseEffect track it’s value when you define the Effect. 59 |

60 |

The example below, starts a counter on first render which can then be stopped and reset.

61 | 62 | 63 |

Subscribe

64 |

65 | Subscribe decorates the dispatcher with a function to call if a particular action is dispatched so that you 66 | can run a task. These are sometimes refered to as a Saga. A saga can be used to orchestrate something in the background 67 | and dispatch further actions. 68 |

69 | -------------------------------------------------------------------------------- /Demo/Pages/Hooks/UseEffect.snip.razor: -------------------------------------------------------------------------------- 1 | @using BlazorHooked 2 | @using System.Threading 3 | @using System.Threading.Tasks 4 | 5 | 6 | @{ 7 | var (state, dispatch) = context.UseReducer(Reducer, new State(new CancellationTokenSource(), 0, true, 0)); 8 | 9 | context.UseEffect(GenerateMessages(dispatch, state!), new object[] { state!.StartCount }); 10 | 11 |

12 | 15 |  Count: @state.Count 16 |

17 | } 18 |
19 | 20 | @code { 21 | public record State(CancellationTokenSource Cancel, int Count, bool IsRunning, int StartCount); 22 | 23 | public record Start(); 24 | public record Stop(); 25 | public record NewMessage(int Count); 26 | 27 | State Reducer(State state, object action) 28 | { 29 | if (action is Stop) 30 | { 31 | state.Cancel.Cancel(); 32 | } 33 | 34 | return action switch 35 | { 36 | Start => state with { Count = 0, Cancel = new CancellationTokenSource(), StartCount = state.StartCount + 1, IsRunning = true }, 37 | NewMessage m => state with { Count = m.Count }, 38 | Stop => state with { IsRunning = false }, 39 | _ => state, 40 | }; 41 | } 42 | 43 | CallbackEffect GenerateMessages(Dispatch dispatcher, State state) => () => 44 | { 45 | Task.Run(async () => 46 | { 47 | var count = 0; 48 | 49 | while (!state.Cancel.IsCancellationRequested) 50 | { 51 | dispatcher(new NewMessage(++count)); 52 | try 53 | { 54 | await Task.Delay(1000, state.Cancel.Token); 55 | } 56 | catch (TaskCanceledException) { } 57 | } 58 | }); 59 | 60 | // Return a way to cancel the looping if the component is disposed 61 | return () => state.Cancel.Cancel(); 62 | }; 63 | } -------------------------------------------------------------------------------- /Demo/Pages/Hooks/UseReducer.snip.razor: -------------------------------------------------------------------------------- 1 | @using BlazorHooked 2 | @code { 3 | public record CounterState(int Count); 4 | 5 | public record Increment(); 6 | public record Decrement(); 7 | 8 | private CounterState Reducer(CounterState state, object action) => action switch 9 | { 10 | Increment => state with { Count = state.Count + 1 }, 11 | Decrement => state with { Count = state.Count - 1 }, 12 | _ => state, 13 | }; 14 | } 15 | 16 | 17 | @{ 18 | var (state, dispatch) = context.UseReducer(Reducer, new CounterState(0)); 19 | } 20 | 21 |

Count: @state!.Count

22 |

23 | 24 | 25 |

26 |
-------------------------------------------------------------------------------- /Demo/Pages/Hooks/UseState.snip.razor: -------------------------------------------------------------------------------- 1 | @using BlazorHooked 2 | 3 | 4 | @{ 5 | var (count, setCount) = context.UseState(0); 6 | } 7 | 8 |

Count: @count

9 |

10 | 11 | 12 |

13 |
-------------------------------------------------------------------------------- /Demo/Pages/Hooks/WithSaga.snip.razor: -------------------------------------------------------------------------------- 1 | @using BlazorHooked 2 | @code { 3 | public record CounterState(int Count, string? Message); 4 | 5 | public record Increment(); 6 | public record Decrement(); 7 | public record SetMessage(string Message); 8 | 9 | private CounterState Reducer(CounterState state, object action) => action switch 10 | { 11 | Increment => state with { Count = state.Count + 1 }, 12 | Decrement => state with { Count = state.Count - 1 }, 13 | SetMessage m => state with { Message = m.Message }, 14 | _ => state, 15 | }; 16 | } 17 | 18 | 19 | @{ 20 | var (state, dispatch) = context.UseReducer(Reducer, new CounterState(0, null)) 21 | .Subscribe((action, dispatch) => Task.Run(() => dispatch(new SetMessage("You incemented last time")))) 22 | .Subscribe((action, dispatch) => Task.Run(() => dispatch(new SetMessage("You decremented last time")))); 23 | } 24 | 25 |

Count: @state!.Count

26 |

27 | 28 | 29 |

30 |

Message: @state.Message

31 |
-------------------------------------------------------------------------------- /Demo/Pages/Index/BaseComponent.snip.razor: -------------------------------------------------------------------------------- 1 | @inherits HookComponentBase 2 | 3 | @{ 4 | this.Hook.UseState(0); 5 | } -------------------------------------------------------------------------------- /Demo/Pages/Index/GlobalStore.snip.razor: -------------------------------------------------------------------------------- 1 | @code { 2 | public record AppState(); 3 | 4 | private Reducer RootReducer(Reducer reducer) => reducer; 5 | } 6 | 7 | new AppState())> 8 | 9 | 10 | 11 | 12 | 13 | 14 | Not found 15 | 16 |

Sorry, there's nothing at this address.

17 |
18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /Demo/Pages/Index/HookComponent.snip.razor: -------------------------------------------------------------------------------- 1 |  2 | @{ 3 | context.UseState(0); 4 | } 5 |
Hello
6 |
-------------------------------------------------------------------------------- /Demo/Pages/Index/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | Blazor Hooked 4 | 5 |

6 | Blazor Hooked
7 | BlazorHooked Logo 8 |

9 |
10 |
11 |

12 | A minimal boiler-plate, state management framework for Blazor that resembles React Hooks. 13 |

14 |
15 |
16 |

17 |

Get Started

18 |

19 |

20 | Install from Nuget 21 | NuGet Status 22 |

23 |

Add the obligitory @@using BlazorHooked statement to _Imports.razor.

24 |

If you're intending on using a global store wrap your app in your App.razor.

25 | 26 |

The HookContext

27 |

28 | Hooks are accessed via a HookContext which you can get one of two ways. 29 |

30 | 31 |

32 | Inherit from HookComponentBase in which case this.Hook 33 | exposes a single HookContext for the child component. 34 |

35 | 36 |

37 | Or use the Hook component, in which case the HookContext is scoped 38 | within the Hook. This gives more flexibility for you to inherit from other 39 | base components and even to create multiple contexts within a component. 40 |

41 | 42 |

You can rename the context to something more helpful and/or to avoid collisions.

43 | 44 |

45 | You’ll find there are very few classes or interfaces to inherit or implement 46 | in BlazorHooked. Actions and state in the examples are usually defined as records. 47 | The more you embrace immutibility the easier the Model View Update pattern becomes 48 | because you stop fighting the render loop and BlazorHooked is designed to foster 49 | that by using functional constructs wherever possible. 50 |

51 |

Click below to learn about the Hooks we currently provide.

52 |

53 | Hooks 54 |

55 | -------------------------------------------------------------------------------- /Demo/Pages/Index/MultipleHooks.snip.razor: -------------------------------------------------------------------------------- 1 |  2 | @{ 3 | var (state, _) = Hook.UseState(0); 4 | } 5 | 6 | @state 7 | 8 | 9 | @{ 10 | var (state, _) = Hook2.UseState(1); 11 | } 12 | 13 | @state 14 | -------------------------------------------------------------------------------- /Demo/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.Web; 2 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 3 | using Demo; 4 | using Havit.Blazor.Components.Web; 5 | 6 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 7 | builder.RootComponents.Add("#app"); 8 | builder.RootComponents.Add("head::after"); 9 | builder.Services.AddHxServices(); 10 | 11 | await builder.Build().RunAsync(); 12 | -------------------------------------------------------------------------------- /Demo/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Demo": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": false, 7 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 8 | "applicationUrl": "https://localhost:7147;http://localhost:5163", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Demo/Shared/Code.razor: -------------------------------------------------------------------------------- 1 | @inject IJSRuntime Javascript 2 | 3 | @code { 4 | [Parameter] 5 | public string Language { get; init; } = "razor"; 6 | 7 | [Parameter, EditorRequired] 8 | public Type Snippet { get; init; } = null!; 9 | 10 | private string? Content { get; set; } 11 | 12 | protected override async Task OnParametersSetAsync() 13 | { 14 | base.OnParametersSet(); 15 | 16 | var resourceName = Snippet.FullName!.Replace("_snip", ".snip.razor"); 17 | using var stream = Snippet.Assembly.GetManifestResourceStream(resourceName); 18 | using var content = new StreamReader(stream!); 19 | 20 | Content = await content.ReadToEndAsync(); 21 | } 22 | 23 | protected override async Task OnAfterRenderAsync(bool firstRender) 24 | { 25 | await base.OnAfterRenderAsync(firstRender); 26 | await Javascript.InvokeVoidAsync("highlightCode"); 27 | } 28 | } 29 | 30 |
31 | 
32 |     @Content
33 | 
34 | 
-------------------------------------------------------------------------------- /Demo/Shared/CodeDemo.razor: -------------------------------------------------------------------------------- 1 | @inject IJSRuntime Javascript 2 | 3 | @code { 4 | [Parameter] 5 | public string Language { get; init; } = "razor"; 6 | 7 | [Parameter, EditorRequired] 8 | public Type Snippet { get; init; } = null!; 9 | } 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Demo/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | 4 | 5 | BlazorHooked 6 | Blazor Hooked 7 | 8 | 9 | 10 | 11 | Home 12 | Hooks 13 | Store 14 | Helpers 15 | GitHub 16 | 17 | 18 | 19 | 20 |
21 |
22 | @Body 23 |
24 |
25 | 26 | 27 |
28 | © 2022 James Dibble 29 |
30 |
-------------------------------------------------------------------------------- /Demo/Shared/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row ::deep .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | text-decoration: none; 28 | } 29 | 30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .top-row ::deep a:first-child { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | @media (max-width: 640.98px) { 40 | .top-row:not(.auth) { 41 | display: none; 42 | } 43 | 44 | .top-row.auth { 45 | justify-content: space-between; 46 | } 47 | 48 | .top-row ::deep a, .top-row ::deep .btn-link { 49 | margin-left: 0; 50 | } 51 | } 52 | 53 | @media (min-width: 641px) { 54 | .page { 55 | flex-direction: row; 56 | } 57 | 58 | .sidebar { 59 | width: 250px; 60 | height: 100vh; 61 | position: sticky; 62 | top: 0; 63 | } 64 | 65 | .top-row { 66 | position: sticky; 67 | top: 0; 68 | z-index: 1; 69 | } 70 | 71 | .top-row.auth ::deep a:first-child { 72 | flex: 1; 73 | text-align: right; 74 | width: 0; 75 | } 76 | 77 | .top-row, article { 78 | padding-left: 2rem !important; 79 | padding-right: 1.5rem !important; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Demo/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 8 | @using Microsoft.JSInterop 9 | @using Demo 10 | @using Demo.Shared 11 | @using BlazorHooked 12 | @using Havit.Blazor.Components.Web 13 | @using Havit.Blazor.Components.Web.Bootstrap -------------------------------------------------------------------------------- /Demo/libman.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "defaultProvider": "cdnjs", 4 | "libraries": [ 5 | { 6 | "provider": "cdnjs", 7 | "library": "prism@9000.0.1", 8 | "destination": "wwwroot/lib/prism/", 9 | "files": [ 10 | "prism.min.js", 11 | "themes/prism.min.css" 12 | ] 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /Demo/wwwroot/css/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; 3 | margin: 0; 4 | background: #181a1b; 5 | color: #eee; 6 | } 7 | 8 | article { 9 | margin-top: 1rem; 10 | margin-bottom: 1.5rem; 11 | min-height: calc(100vh - 5.5rem - 48px); 12 | } 13 | 14 | .loading { 15 | min-height: 100vh; 16 | width: 100%; 17 | position: fixed; 18 | top: 0; 19 | display: flex; 20 | color: #fff; 21 | align-items: center; 22 | text-align: center; 23 | justify-content: center; 24 | flex-direction: column; 25 | } 26 | 27 | .loading p { 28 | margin: 0; 29 | } 30 | 31 | #blazor-error-ui { 32 | background: lightyellow; 33 | bottom: 0; 34 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 35 | display: none; 36 | left: 0; 37 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 38 | position: fixed; 39 | width: 100%; 40 | z-index: 1000; 41 | } 42 | 43 | #blazor-error-ui .dismiss { 44 | cursor: pointer; 45 | position: absolute; 46 | right: 0.75rem; 47 | top: 0.5rem; 48 | } 49 | 50 | .blazor-error-boundary { 51 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 52 | padding: 1rem 1rem 1rem 3.7rem; 53 | color: white; 54 | } 55 | 56 | .blazor-error-boundary::after { 57 | content: "An error has occurred." 58 | } 59 | 60 | /* PrismJS 1.26.0 */ 61 | code[class*=language-], pre[class*=language-] { 62 | color: #ccc; 63 | background: 0 0; 64 | font-family: Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace; 65 | font-size: 1em; 66 | text-align: left; 67 | white-space: pre; 68 | word-spacing: normal; 69 | word-break: normal; 70 | word-wrap: normal; 71 | line-height: 1.5; 72 | -moz-tab-size: 4; 73 | -o-tab-size: 4; 74 | tab-size: 4; 75 | -webkit-hyphens: none; 76 | -moz-hyphens: none; 77 | -ms-hyphens: none; 78 | hyphens: none 79 | } 80 | 81 | pre[class*=language-] { 82 | padding: 1em; 83 | margin: .5em 0; 84 | overflow: auto 85 | } 86 | 87 | :not(pre) > code[class*=language-], pre[class*=language-] { 88 | background: #2d2d2d 89 | } 90 | 91 | :not(pre) > code[class*=language-] { 92 | padding: .1em; 93 | border-radius: .3em; 94 | white-space: normal 95 | } 96 | 97 | .token.block-comment, .token.cdata, .token.comment, .token.doctype, .token.prolog { 98 | color: #999 99 | } 100 | 101 | .token.punctuation { 102 | color: #ccc 103 | } 104 | 105 | .token.attr-name, .token.deleted, .token.namespace, .token.tag { 106 | color: #e2777a 107 | } 108 | 109 | .token.function-name { 110 | color: #6196cc 111 | } 112 | 113 | .token.boolean, .token.function, .token.number { 114 | color: #f08d49 115 | } 116 | 117 | .token.class-name, .token.constant, .token.property, .token.symbol { 118 | color: #f8c555 119 | } 120 | 121 | .token.atrule, .token.builtin, .token.important, .token.keyword, .token.selector { 122 | color: #cc99cd 123 | } 124 | 125 | .token.attr-value, .token.char, .token.regex, .token.string, .token.variable { 126 | color: #7ec699 127 | } 128 | 129 | .token.entity, .token.operator, .token.url { 130 | color: #67cdcc 131 | } 132 | 133 | .token.bold, .token.important { 134 | font-weight: 700 135 | } 136 | 137 | .token.italic { 138 | font-style: italic 139 | } 140 | 141 | .token.entity { 142 | cursor: help 143 | } 144 | 145 | .token.inserted { 146 | color: green 147 | } 148 | -------------------------------------------------------------------------------- /Demo/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Blazor Hooked 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
77 |

Blazor Hooked

78 |

Loading...

79 | BlazorHooked Logo 80 |
81 |
82 | 83 |
84 | An unhandled error has occurred. 85 | Reload 86 | 🗙 87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /Demo/wwwroot/js/app.js: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.26.0 2 | https://prismjs.com/download.html#themes=prism-dark&languages=markup+clike+csharp+cshtml */ 3 | var _self = "undefined" != typeof window ? window : "undefined" != typeof WorkerGlobalScope && self instanceof WorkerGlobalScope ? self : {}, Prism = function (u) { var t = /(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i, n = 0, e = {}, M = { manual: u.Prism && u.Prism.manual, disableWorkerMessageHandler: u.Prism && u.Prism.disableWorkerMessageHandler, util: { encode: function e(n) { return n instanceof W ? new W(n.type, e(n.content), n.alias) : Array.isArray(n) ? n.map(e) : n.replace(/&/g, "&").replace(/= l.reach); y += m.value.length, m = m.next) { var k = m.value; if (t.length > n.length) return; if (!(k instanceof W)) { var x, b = 1; if (h) { if (!(x = z(p, y, n, f)) || x.index >= n.length) break; var w = x.index, A = x.index + x[0].length, E = y; for (E += m.value.length; E <= w;)m = m.next, E += m.value.length; if (E -= m.value.length, y = E, m.value instanceof W) continue; for (var P = m; P !== t.tail && (E < A || "string" == typeof P.value); P = P.next)b++, E += P.value.length; b--, k = n.slice(y, E), x.index -= y } else if (!(x = z(p, 0, k, f))) continue; var w = x.index, L = x[0], S = k.slice(0, w), O = k.slice(w + L.length), j = y + k.length; l && j > l.reach && (l.reach = j); var C = m.prev; S && (C = I(t, C, S), y += S.length), T(t, C, b); var N = new W(o, g ? M.tokenize(L, g) : L, d, L); if (m = I(t, C, N), O && I(t, m, O), 1 < b) { var _ = { cause: o + "," + u, reach: j }; e(n, t, r, m.prev, y, _), l && _.reach > l.reach && (l.reach = _.reach) } } } } } }(e, a, n, a.head, 0), function (e) { var n = [], t = e.head.next; for (; t !== e.tail;)n.push(t.value), t = t.next; return n }(a) }, hooks: { all: {}, add: function (e, n) { var t = M.hooks.all; t[e] = t[e] || [], t[e].push(n) }, run: function (e, n) { var t = M.hooks.all[e]; if (t && t.length) for (var r, a = 0; r = t[a++];)r(n) } }, Token: W }; function W(e, n, t, r) { this.type = e, this.content = n, this.alias = t, this.length = 0 | (r || "").length } function z(e, n, t, r) { e.lastIndex = n; var a = e.exec(t); if (a && r && a[1]) { var i = a[1].length; a.index += i, a[0] = a[0].slice(i) } return a } function i() { var e = { value: null, prev: null, next: null }, n = { value: null, prev: e, next: null }; e.next = n, this.head = e, this.tail = n, this.length = 0 } function I(e, n, t) { var r = n.next, a = { value: t, prev: n, next: r }; return n.next = a, r.prev = a, e.length++, a } function T(e, n, t) { for (var r = n.next, a = 0; a < t && r !== e.tail; a++)r = r.next; (n.next = r).prev = n, e.length -= a } if (u.Prism = M, W.stringify = function n(e, t) { if ("string" == typeof e) return e; if (Array.isArray(e)) { var r = ""; return e.forEach(function (e) { r += n(e, t) }), r } var a = { type: e.type, content: n(e.content, t), tag: "span", classes: ["token", e.type], attributes: {}, language: t }, i = e.alias; i && (Array.isArray(i) ? Array.prototype.push.apply(a.classes, i) : a.classes.push(i)), M.hooks.run("wrap", a); var l = ""; for (var o in a.attributes) l += " " + o + '="' + (a.attributes[o] || "").replace(/"/g, """) + '"'; return "<" + a.tag + ' class="' + a.classes.join(" ") + '"' + l + ">" + a.content + "" }, !u.document) return u.addEventListener && (M.disableWorkerMessageHandler || u.addEventListener("message", function (e) { var n = JSON.parse(e.data), t = n.language, r = n.code, a = n.immediateClose; u.postMessage(M.highlight(r, M.languages[t], t)), a && u.close() }, !1)), M; var r = M.util.currentScript(); function a() { M.manual || M.highlightAll() } if (r && (M.filename = r.src, r.hasAttribute("data-manual") && (M.manual = !0)), !M.manual) { var l = document.readyState; "loading" === l || "interactive" === l && r && r.defer ? document.addEventListener("DOMContentLoaded", a) : window.requestAnimationFrame ? window.requestAnimationFrame(a) : window.setTimeout(a, 16) } return M }(_self); "undefined" != typeof module && module.exports && (module.exports = Prism), "undefined" != typeof global && (global.Prism = Prism); 4 | Prism.languages.markup = { comment: { pattern: //, greedy: !0 }, prolog: { pattern: /<\?[\s\S]+?\?>/, greedy: !0 }, doctype: { pattern: /"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i, greedy: !0, inside: { "internal-subset": { pattern: /(^[^\[]*\[)[\s\S]+(?=\]>$)/, lookbehind: !0, greedy: !0, inside: null }, string: { pattern: /"[^"]*"|'[^']*'/, greedy: !0 }, punctuation: /^$|[[\]]/, "doctype-tag": /^DOCTYPE/i, name: /[^\s<>'"]+/ } }, cdata: { pattern: //i, greedy: !0 }, tag: { pattern: /<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/, greedy: !0, inside: { tag: { pattern: /^<\/?[^\s>\/]+/, inside: { punctuation: /^<\/?/, namespace: /^[^\s>\/:]+:/ } }, "special-attr": [], "attr-value": { pattern: /=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/, inside: { punctuation: [{ pattern: /^=/, alias: "attr-equals" }, /"|'/] } }, punctuation: /\/?>/, "attr-name": { pattern: /[^\s>\/]+/, inside: { namespace: /^[^\s>\/:]+:/ } } } }, entity: [{ pattern: /&[\da-z]{1,8};/i, alias: "named-entity" }, /&#x?[\da-f]{1,8};/i] }, Prism.languages.markup.tag.inside["attr-value"].inside.entity = Prism.languages.markup.entity, Prism.languages.markup.doctype.inside["internal-subset"].inside = Prism.languages.markup, Prism.hooks.add("wrap", function (a) { "entity" === a.type && (a.attributes.title = a.content.replace(/&/, "&")) }), Object.defineProperty(Prism.languages.markup.tag, "addInlined", { value: function (a, e) { var s = {}; s["language-" + e] = { pattern: /(^$)/i, lookbehind: !0, inside: Prism.languages[e] }, s.cdata = /^$/i; var t = { "included-cdata": { pattern: //i, inside: s } }; t["language-" + e] = { pattern: /[\s\S]+/, inside: Prism.languages[e] }; var n = {}; n[a] = { pattern: RegExp("(<__[^>]*>)(?:))*\\]\\]>|(?!)".replace(/__/g, function () { return a }), "i"), lookbehind: !0, greedy: !0, inside: t }, Prism.languages.insertBefore("markup", "cdata", n) } }), Object.defineProperty(Prism.languages.markup.tag, "addAttribute", { value: function (a, e) { Prism.languages.markup.tag.inside["special-attr"].push({ pattern: RegExp("(^|[\"'\\s])(?:" + a + ")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))", "i"), lookbehind: !0, inside: { "attr-name": /^[^\s=]+/, "attr-value": { pattern: /=[\s\S]+/, inside: { value: { pattern: /(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/, lookbehind: !0, alias: [e, "language-" + e], inside: Prism.languages[e] }, punctuation: [{ pattern: /^=/, alias: "attr-equals" }, /"|'/] } } } }) } }), Prism.languages.html = Prism.languages.markup, Prism.languages.mathml = Prism.languages.markup, Prism.languages.svg = Prism.languages.markup, Prism.languages.xml = Prism.languages.extend("markup", {}), Prism.languages.ssml = Prism.languages.xml, Prism.languages.atom = Prism.languages.xml, Prism.languages.rss = Prism.languages.xml; 5 | Prism.languages.clike = { comment: [{ pattern: /(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/, lookbehind: !0, greedy: !0 }, { pattern: /(^|[^\\:])\/\/.*/, lookbehind: !0, greedy: !0 }], string: { pattern: /(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/, greedy: !0 }, "class-name": { pattern: /(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i, lookbehind: !0, inside: { punctuation: /[.\\]/ } }, keyword: /\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/, boolean: /\b(?:false|true)\b/, function: /\b\w+(?=\()/, number: /\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i, operator: /[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/, punctuation: /[{}[\];(),.:]/ }; 6 | !function (s) { function a(e, s) { return e.replace(/<<(\d+)>>/g, function (e, n) { return "(?:" + s[+n] + ")" }) } function t(e, n, s) { return RegExp(a(e, n), s || "") } function e(e, n) { for (var s = 0; s < n; s++)e = e.replace(/<>/g, function () { return "(?:" + e + ")" }); return e.replace(/<>/g, "[^\\s\\S]") } var n = "bool byte char decimal double dynamic float int long object sbyte short string uint ulong ushort var void", r = "class enum interface record struct", i = "add alias and ascending async await by descending from(?=\\s*(?:\\w|$)) get global group into init(?=\\s*;) join let nameof not notnull on or orderby partial remove select set unmanaged value when where with(?=\\s*{)", o = "abstract as base break case catch checked const continue default delegate do else event explicit extern finally fixed for foreach goto if implicit in internal is lock namespace new null operator out override params private protected public readonly ref return sealed sizeof stackalloc static switch this throw try typeof unchecked unsafe using virtual volatile while yield"; function l(e) { return "\\b(?:" + e.trim().replace(/ /g, "|") + ")\\b" } var d = l(r), p = RegExp(l(n + " " + r + " " + i + " " + o)), c = l(r + " " + i + " " + o), u = l(n + " " + r + " " + o), g = e("<(?:[^<>;=+\\-*/%&|^]|<>)*>", 2), b = e("\\((?:[^()]|<>)*\\)", 2), h = "@?\\b[A-Za-z_]\\w*\\b", f = a("<<0>>(?:\\s*<<1>>)?", [h, g]), m = a("(?!<<0>>)<<1>>(?:\\s*\\.\\s*<<1>>)*", [c, f]), k = "\\[\\s*(?:,\\s*)*\\]", y = a("<<0>>(?:\\s*(?:\\?\\s*)?<<1>>)*(?:\\s*\\?)?", [m, k]), w = a("(?:<<0>>|<<1>>)(?:\\s*(?:\\?\\s*)?<<2>>)*(?:\\s*\\?)?", [a("\\(<<0>>+(?:,<<0>>+)+\\)", [a("[^,()<>[\\];=+\\-*/%&|^]|<<0>>|<<1>>|<<2>>", [g, b, k])]), m, k]), v = { keyword: p, punctuation: /[<>()?,.:[\]]/ }, x = "'(?:[^\r\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'", $ = '"(?:\\\\.|[^\\\\"\r\n])*"'; s.languages.csharp = s.languages.extend("clike", { string: [{ pattern: t("(^|[^$\\\\])<<0>>", ['@"(?:""|\\\\[^]|[^\\\\"])*"(?!")']), lookbehind: !0, greedy: !0 }, { pattern: t("(^|[^@$\\\\])<<0>>", [$]), lookbehind: !0, greedy: !0 }], "class-name": [{ pattern: t("(\\busing\\s+static\\s+)<<0>>(?=\\s*;)", [m]), lookbehind: !0, inside: v }, { pattern: t("(\\busing\\s+<<0>>\\s*=\\s*)<<1>>(?=\\s*;)", [h, w]), lookbehind: !0, inside: v }, { pattern: t("(\\busing\\s+)<<0>>(?=\\s*=)", [h]), lookbehind: !0 }, { pattern: t("(\\b<<0>>\\s+)<<1>>", [d, f]), lookbehind: !0, inside: v }, { pattern: t("(\\bcatch\\s*\\(\\s*)<<0>>", [m]), lookbehind: !0, inside: v }, { pattern: t("(\\bwhere\\s+)<<0>>", [h]), lookbehind: !0 }, { pattern: t("(\\b(?:is(?:\\s+not)?|as)\\s+)<<0>>", [y]), lookbehind: !0, inside: v }, { pattern: t("\\b<<0>>(?=\\s+(?!<<1>>|with\\s*\\{)<<2>>(?:\\s*[=,;:{)\\]]|\\s+(?:in|when)\\b))", [w, u, h]), inside: v }], keyword: p, number: /(?:\b0(?:x[\da-f_]*[\da-f]|b[01_]*[01])|(?:\B\.\d+(?:_+\d+)*|\b\d+(?:_+\d+)*(?:\.\d+(?:_+\d+)*)?)(?:e[-+]?\d+(?:_+\d+)*)?)(?:[dflmu]|lu|ul)?\b/i, operator: />>=?|<<=?|[-=]>|([-+&|])\1|~|\?\?=?|[-+*/%&|^!=<>]=?/, punctuation: /\?\.?|::|[{}[\];(),.:]/ }), s.languages.insertBefore("csharp", "number", { range: { pattern: /\.\./, alias: "operator" } }), s.languages.insertBefore("csharp", "punctuation", { "named-parameter": { pattern: t("([(,]\\s*)<<0>>(?=\\s*:)", [h]), lookbehind: !0, alias: "punctuation" } }), s.languages.insertBefore("csharp", "class-name", { namespace: { pattern: t("(\\b(?:namespace|using)\\s+)<<0>>(?:\\s*\\.\\s*<<0>>)*(?=\\s*[;{])", [h]), lookbehind: !0, inside: { punctuation: /\./ } }, "type-expression": { pattern: t("(\\b(?:default|sizeof|typeof)\\s*\\(\\s*(?!\\s))(?:[^()\\s]|\\s(?!\\s)|<<0>>)*(?=\\s*\\))", [b]), lookbehind: !0, alias: "class-name", inside: v }, "return-type": { pattern: t("<<0>>(?=\\s+(?:<<1>>\\s*(?:=>|[({]|\\.\\s*this\\s*\\[)|this\\s*\\[))", [w, m]), inside: v, alias: "class-name" }, "constructor-invocation": { pattern: t("(\\bnew\\s+)<<0>>(?=\\s*[[({])", [w]), lookbehind: !0, inside: v, alias: "class-name" }, "generic-method": { pattern: t("<<0>>\\s*<<1>>(?=\\s*\\()", [h, g]), inside: { function: t("^<<0>>", [h]), generic: { pattern: RegExp(g), alias: "class-name", inside: v } } }, "type-list": { pattern: t("\\b((?:<<0>>\\s+<<1>>|record\\s+<<1>>\\s*<<5>>|where\\s+<<2>>)\\s*:\\s*)(?:<<3>>|<<4>>|<<1>>\\s*<<5>>|<<6>>)(?:\\s*,\\s*(?:<<3>>|<<4>>|<<6>>))*(?=\\s*(?:where|[{;]|=>|$))", [d, f, h, w, p.source, b, "\\bnew\\s*\\(\\s*\\)"]), lookbehind: !0, inside: { "record-arguments": { pattern: t("(^(?!new\\s*\\()<<0>>\\s*)<<1>>", [f, b]), lookbehind: !0, greedy: !0, inside: s.languages.csharp }, keyword: p, "class-name": { pattern: RegExp(w), greedy: !0, inside: v }, punctuation: /[,()]/ } }, preprocessor: { pattern: /(^[\t ]*)#.*/m, lookbehind: !0, alias: "property", inside: { directive: { pattern: /(#)\b(?:define|elif|else|endif|endregion|error|if|line|nullable|pragma|region|undef|warning)\b/, lookbehind: !0, alias: "keyword" } } } }); var _ = $ + "|" + x, B = a("/(?![*/])|//[^\r\n]*[\r\n]|/\\*(?:[^*]|\\*(?!/))*\\*/|<<0>>", [_]), E = e(a("[^\"'/()]|<<0>>|\\(<>*\\)", [B]), 2), R = "\\b(?:assembly|event|field|method|module|param|property|return|type)\\b", z = a("<<0>>(?:\\s*\\(<<1>>*\\))?", [m, E]); s.languages.insertBefore("csharp", "class-name", { attribute: { pattern: t("((?:^|[^\\s\\w>)?])\\s*\\[\\s*)(?:<<0>>\\s*:\\s*)?<<1>>(?:\\s*,\\s*<<1>>)*(?=\\s*\\])", [R, z]), lookbehind: !0, greedy: !0, inside: { target: { pattern: t("^<<0>>(?=\\s*:)", [R]), alias: "keyword" }, "attribute-arguments": { pattern: t("\\(<<0>>*\\)", [E]), inside: s.languages.csharp }, "class-name": { pattern: RegExp(m), inside: { punctuation: /\./ } }, punctuation: /[:,]/ } } }); var S = ":[^}\r\n]+", j = e(a("[^\"'/()]|<<0>>|\\(<>*\\)", [B]), 2), A = a("\\{(?!\\{)(?:(?![}:])<<0>>)*<<1>>?\\}", [j, S]), F = e(a("[^\"'/()]|/(?!\\*)|/\\*(?:[^*]|\\*(?!/))*\\*/|<<0>>|\\(<>*\\)", [_]), 2), P = a("\\{(?!\\{)(?:(?![}:])<<0>>)*<<1>>?\\}", [F, S]); function U(e, n) { return { interpolation: { pattern: t("((?:^|[^{])(?:\\{\\{)*)<<0>>", [e]), lookbehind: !0, inside: { "format-string": { pattern: t("(^\\{(?:(?![}:])<<0>>)*)<<1>>(?=\\}$)", [n, S]), lookbehind: !0, inside: { punctuation: /^:/ } }, punctuation: /^\{|\}$/, expression: { pattern: /[\s\S]+/, alias: "language-csharp", inside: s.languages.csharp } } }, string: /[\s\S]+/ } } s.languages.insertBefore("csharp", "string", { "interpolation-string": [{ pattern: t('(^|[^\\\\])(?:\\$@|@\\$)"(?:""|\\\\[^]|\\{\\{|<<0>>|[^\\\\{"])*"', [A]), lookbehind: !0, greedy: !0, inside: U(A, j) }, { pattern: t('(^|[^@\\\\])\\$"(?:\\\\.|\\{\\{|<<0>>|[^\\\\"{])*"', [P]), lookbehind: !0, greedy: !0, inside: U(P, F) }], char: { pattern: RegExp(x), greedy: !0 } }), s.languages.dotnet = s.languages.cs = s.languages.csharp }(Prism); 7 | !function (e) { function s(e, s) { for (var a = 0; a < s; a++)e = e.replace(//g, function () { return "(?:" + e + ")" }); return e.replace(//g, "[^\\s\\S]").replace(//g, '(?:@(?!")|"(?:[^\r\n\\\\"]|\\\\.)*"|@"(?:[^\\\\"]|""|\\\\[^])*"(?!")|' + "'(?:(?:[^\r\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'|(?=[^\\\\](?!'))))").replace(//g, "(?:/(?![/*])|//.*[\r\n]|/\\*[^*]*(?:\\*(?!/)[^*]*)*\\*/)") } var a = s("\\((?:[^()'\"@/]|||)*\\)", 2), r = s("\\[(?:[^\\[\\]'\"@/]|||)*\\]", 2), t = s("\\{(?:[^{}'\"@/]|||)*\\}", 2), n = s("<(?:[^<>'\"@/]|||)*>", 2), l = "(?:\\s(?:\\s*[^\\s>/=]+(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))|(?=[\\s/>])))+)?", i = "(?!\\d)[^\\s>/=$<%]+" + l + "\\s*/?>", o = "\\B@?(?:<([a-zA-Z][\\w:]*)" + l + "\\s*>(?:[^<]|(?:[^<]|)*", 2) + ")*|<" + i + ")"; e.languages.cshtml = e.languages.extend("markup", {}); var g = { pattern: /\S[\s\S]*/, alias: "language-csharp", inside: e.languages.insertBefore("csharp", "string", { html: { pattern: RegExp(o), greedy: !0, inside: e.languages.cshtml } }, { csharp: e.languages.extend("csharp", {}) }) }; e.languages.insertBefore("cshtml", "prolog", { "razor-comment": { pattern: /@\*[\s\S]*?\*@/, greedy: !0, alias: "comment" }, block: { pattern: RegExp("(^|[^@])@(?:" + [t, "(?:code|functions)\\s*" + t, "(?:for|foreach|lock|switch|using|while)\\s*" + a + "\\s*" + t, "do\\s*" + t + "\\s*while\\s*" + a + "(?:\\s*;)?", "try\\s*" + t + "\\s*catch\\s*" + a + "\\s*" + t + "\\s*finally\\s*" + t, "if\\s*" + a + "\\s*" + t + "(?:\\s*else(?:\\s+if\\s*" + a + ")?\\s*" + t + ")*"].join("|") + ")"), lookbehind: !0, greedy: !0, inside: { keyword: /^@\w*/, csharp: g } }, directive: { pattern: /^([ \t]*)@(?:addTagHelper|attribute|implements|inherits|inject|layout|model|namespace|page|preservewhitespace|removeTagHelper|section|tagHelperPrefix|using)(?=\s).*/m, lookbehind: !0, greedy: !0, inside: { keyword: /^@\w+/, csharp: g } }, value: { pattern: RegExp("(^|[^@])@(?:await\\b\\s*)?(?:\\w+\\b|" + a + ")(?:[?!]?\\.\\w+\\b|" + a + "|" + r + "|" + n + a + ")*"), lookbehind: !0, greedy: !0, alias: "variable", inside: { keyword: /^@/, csharp: g } }, "delegate-operator": { pattern: /(^|[^@])@(?=<)/, lookbehind: !0, alias: "operator" } }), e.languages.razor = e.languages.cshtml }(Prism); 8 | 9 | window.highlightCode = function() { 10 | Prism.highlightAll(); 11 | } -------------------------------------------------------------------------------- /Demo/wwwroot/lib/prism/prism.min.js: -------------------------------------------------------------------------------- 1 | self=typeof window!=="undefined"?window:typeof WorkerGlobalScope!=="undefined"&&self instanceof WorkerGlobalScope?self:{};var Prism=function(){var lang=/\blang(?:uage)?-(?!\*)(\w+)\b/i;var _=self.Prism={util:{encode:function(tokens){if(tokens instanceof Token){return new Token(tokens.type,_.util.encode(tokens.content),tokens.alias)}else if(_.util.type(tokens)==="Array"){return tokens.map(_.util.encode)}else{return tokens.replace(/&/g,"&").replace(/text.length){break tokenloop}if(str instanceof Token){continue}pattern.lastIndex=0;var match=pattern.exec(str);if(match){if(lookbehind){lookbehindLength=match[1].length}var from=match.index-1+lookbehindLength,match=match[0].slice(lookbehindLength),len=match.length,to=from+len,before=str.slice(0,from+1),after=str.slice(to+1);var args=[i,1];if(before){args.push(before)}var wrapped=new Token(token,inside?_.tokenize(match,inside):match,alias);args.push(wrapped);if(after){args.push(after)}Array.prototype.splice.apply(strarr,args)}}}}return strarr},hooks:{all:{},add:function(name,callback){var hooks=_.hooks.all;hooks[name]=hooks[name]||[];hooks[name].push(callback)},run:function(name,env){var callbacks=_.hooks.all[name];if(!callbacks||!callbacks.length){return}for(var i=0,callback;callback=callbacks[i++];){callback(env)}}}};var Token=_.Token=function(type,content,alias){this.type=type;this.content=content;this.alias=alias};Token.stringify=function(o,language,parent){if(typeof o=="string"){return o}if(_.util.type(o)==="Array"){return o.map(function(element){return Token.stringify(element,language,o)}).join("")}var env={type:o.type,content:Token.stringify(o.content,language,parent),tag:"span",classes:["token",o.type],attributes:{},language:language,parent:parent};if(env.type=="comment"){env.attributes["spellcheck"]="true"}if(o.alias){var aliases=_.util.type(o.alias)==="Array"?o.alias:[o.alias];Array.prototype.push.apply(env.classes,aliases)}_.hooks.run("wrap",env);var attributes="";for(var name in env.attributes){attributes+=name+'="'+(env.attributes[name]||"")+'"'}return"<"+env.tag+' class="'+env.classes.join(" ")+'" '+attributes+">"+env.content+""};if(!self.document){if(!self.addEventListener){return self.Prism}self.addEventListener("message",function(evt){var message=JSON.parse(evt.data),lang=message.language,code=message.code;self.postMessage(JSON.stringify(_.util.encode(_.tokenize(code,_.languages[lang]))));self.close()},false);return self.Prism}var script=document.getElementsByTagName("script");script=script[script.length-1];if(script){_.filename=script.src;if(document.addEventListener&&!script.hasAttribute("data-manual")){document.addEventListener("DOMContentLoaded",_.highlightAll)}}return self.Prism}();if(typeof module!=="undefined"&&module.exports){module.exports=Prism}Prism.languages.markup={comment://,prolog:/<\?.+?\?>/,doctype://,cdata://i,tag:{pattern:/<\/?[\w:-]+\s*(?:\s+[\w:-]+(?:=(?:("|')(\\?[\w\W])*?\1|[^\s'">=]+))?\s*)*\/?>/i,inside:{tag:{pattern:/^<\/?[\w:-]+/i,inside:{punctuation:/^<\/?/,namespace:/^[\w-]+?:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/i,inside:{punctuation:/=|>|"/}},punctuation:/\/?>/,"attr-name":{pattern:/[\w:-]+/,inside:{namespace:/^[\w-]+?:/}}}},entity:/&#?[\da-z]{1,8};/i};Prism.hooks.add("wrap",function(env){if(env.type==="entity"){env.attributes["title"]=env.content.replace(/&/,"&")}});Prism.languages.css={comment:/\/\*[\w\W]*?\*\//,atrule:{pattern:/@[\w-]+?.*?(;|(?=\s*\{))/i,inside:{punctuation:/[;:]/}},url:/url\((?:(["'])(\\\n|\\?.)*?\1|.*?)\)/i,selector:/[^\{\}\s][^\{\};]*(?=\s*\{)/,string:/("|')(\\\n|\\?.)*?\1/,property:/(\b|\B)[\w-]+(?=\s*:)/i,important:/\B!important\b/i,punctuation:/[\{\};:]/,"function":/[-a-z0-9]+(?=\()/i};if(Prism.languages.markup){Prism.languages.insertBefore("markup","tag",{style:{pattern:/[\w\W]*?<\/style>/i,inside:{tag:{pattern:/|<\/style>/i,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.css},alias:"language-css"}});Prism.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|').*?\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:Prism.languages.markup.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:Prism.languages.css}},alias:"language-css"}},Prism.languages.markup.tag)}Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\w\W]*?\*\//,lookbehind:true},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:true}],string:/("|')(\\\n|\\?.)*?\1/,"class-name":{pattern:/((?:(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,lookbehind:true,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(true|false)\b/,"function":{pattern:/[a-z0-9_]+\(/i,inside:{punctuation:/\(/}},number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/,operator:/[-+]{1,2}|!|<=?|>=?|={1,3}|&{1,2}|\|?\||\?|\*|\/|~|\^|%/,ignore:/&(lt|gt|amp);/i,punctuation:/[{}[\];(),.:]/};Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|function|get|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|set|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)\b/,number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|-?Infinity)\b/,"function":/(?!\d)[a-z0-9_$]+(?=\()/i});Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^\/])\/(?!\/)(\[.+?]|\\.|[^\/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/,lookbehind:true}});if(Prism.languages.markup){Prism.languages.insertBefore("markup","tag",{script:{pattern:/[\w\W]*?<\/script>/i,inside:{tag:{pattern:/|<\/script>/i,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.javascript},alias:"language-javascript"}})}(function(){if(!self.Prism||!self.document||!document.querySelector){return}self.Prism.fileHighlight=function(){var Extensions={js:"javascript",html:"markup",svg:"markup",xml:"markup",py:"python",rb:"ruby",ps1:"powershell",psm1:"powershell"};Array.prototype.slice.call(document.querySelectorAll("pre[data-src]")).forEach(function(pre){var src=pre.getAttribute("data-src");var extension=(src.match(/\.(\w+)$/)||[,""])[1];var language=Extensions[extension]||extension;var code=document.createElement("code");code.className="language-"+language;pre.textContent="";code.textContent="Loading…";pre.appendChild(code);var xhr=new XMLHttpRequest;xhr.open("GET",src,true);xhr.onreadystatechange=function(){if(xhr.readyState==4){if(xhr.status<400&&xhr.responseText){code.textContent=xhr.responseText;Prism.highlightElement(code)}else if(xhr.status>=400){code.textContent="✖ Error "+xhr.status+" while fetching file: "+xhr.statusText}else{code.textContent="✖ Error: File does not exist or is empty"}}};xhr.send(null)})};self.Prism.fileHighlight()})(); -------------------------------------------------------------------------------- /Demo/wwwroot/lib/prism/themes/prism.min.css: -------------------------------------------------------------------------------- 1 | code[class*="language-"],pre[class*="language-"]{color:black;text-shadow:0 1px white;font-family:Consolas,Monaco,'Andale Mono',monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*="language-"]::-moz-selection,pre[class*="language-"] ::-moz-selection,code[class*="language-"]::-moz-selection,code[class*="language-"] ::-moz-selection{text-shadow:none;background:#b3d4fc}pre[class*="language-"]::selection,pre[class*="language-"] ::selection,code[class*="language-"]::selection,code[class*="language-"] ::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*="language-"],pre[class*="language-"]{text-shadow:none}}pre[class*="language-"]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*="language-"],pre[class*="language-"]{background:#f5f2f0}:not(pre)>code[class*="language-"]{padding:.1em;border-radius:.3em}.token.comment,.token.prolog,.token.doctype,.token.cdata{color:slategray}.token.punctuation{color:#999}.namespace{opacity:.7}.token.property,.token.tag,.token.boolean,.token.number,.token.constant,.token.symbol,.token.deleted{color:#905}.token.selector,.token.attr-name,.token.string,.token.char,.token.builtin,.token.inserted{color:#690}.token.operator,.token.entity,.token.url,.language-css .token.string,.style .token.string{color:#a67f59;background:hsla(0,0,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.function{color:#dd4a68}.token.regex,.token.important,.token.variable{color:#e90}.token.important,.token.bold{font-weight:bold}.token.italic{font-style:italic}.token.entity{cursor:help} -------------------------------------------------------------------------------- /Demo/wwwroot/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dibble-james/blazor-hooked/1ce028ba51b8e46a0a29e99174c6bd7ba5755a47/Demo/wwwroot/logo.ico -------------------------------------------------------------------------------- /Demo/wwwroot/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dibble-james/blazor-hooked/1ce028ba51b8e46a0a29e99174c6bd7ba5755a47/Demo/wwwroot/logo.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 James Dibble 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blazor Hooked 2 | 3 | [![Nuget](https://github.com/dibble-james/blazor-hooked/actions/workflows/nuget.yml/badge.svg?branch=main)](https://github.com/dibble-james/blazor-hooked/actions/workflows/nuget.yml) 4 | 5 | ## Get Started 6 | 7 | Install from Nuget [![NuGet Badge](https://buildstats.info/nuget/BlazorHooked)](https://www.nuget.org/packages/BlazorHooked/) 8 | 9 | Add the obligitory `@@using BlazorHooked` statement to `_Imports.razor`. 10 | 11 | If you're intending on using a global store wrap your app in your `App.razor`. 12 | 13 | ```razor 14 | @code { 15 | public record AppState(); 16 | 17 | private Reducer RootReducer(Reducer reducer) => reducer; 18 | } 19 | 20 | new AppState())> 21 | 22 | 23 | 24 | 25 | 26 | 27 | Not found 28 | 29 |

Sorry, there's nothing at this address.

30 |
31 |
32 |
33 |
34 | ``` 35 | 36 | ### The `HookContext` 37 | 38 | Hooks are accessed via a `HookContext` which you can get one of two ways. 39 | 40 | Inherit from `HookComponentBase` in which case `this.Hook` exposes a single `HookContext` for the child component. 41 | 42 | ```razor 43 | @inherits HookComponentBase 44 | 45 | @{ 46 | this.Hook.UseState(0); 47 | } 48 | ``` 49 | 50 | Or use the `Hook` component, in which case the `HookContext` is scoped within the `Hook`. This gives more flexibility for 51 | you to inherit from other base components and even to create multiple contexts within a component. 52 | 53 | ```razor 54 | 55 | @{ 56 | context.UseState(0); 57 | } 58 |
Hello
59 |
60 | ``` 61 | 62 | You can rename the context to something more helpful and/or to avoid collisions. 63 | 64 | ```razor 65 | 66 | @{ 67 | var (state, _) = Hook.UseState(0); 68 | } 69 | 70 | @state 71 | 72 | 73 | @{ 74 | var (state, _) = Hook2.UseState(1); 75 | } 76 | 77 | @state 78 | 79 | ``` 80 | 81 | You'll find there are very few classes or interfaces to inherit or implement in BlazorHooked. Actions and state in the 82 | examples are usually defined as records. The more you embrace immutibility the easier the Model View Update pattern becomes 83 | because you stop fighting the render loop and BlazorHooked is designed to foster that by using functional constructs 84 | wherever possible. 85 | 86 | Read on to find out more about [Hooks](https://dibble-james.github.io/blazor-hooked/hooks). 87 | -------------------------------------------------------------------------------- /assets/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dibble-james/blazor-hooked/1ce028ba51b8e46a0a29e99174c6bd7ba5755a47/assets/logo.ico -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dibble-james/blazor-hooked/1ce028ba51b8e46a0a29e99174c6bd7ba5755a47/assets/logo.png -------------------------------------------------------------------------------- /blazor-hooked.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32120.378 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorHooked", "BlazorHooked\BlazorHooked.csproj", "{3237E21F-EFE1-4D29-A164-692109D86C4A}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo", "Demo\Demo.csproj", "{37CEA85B-7E35-4D48-A54B-2451B8842B99}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {3237E21F-EFE1-4D29-A164-692109D86C4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {3237E21F-EFE1-4D29-A164-692109D86C4A}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {3237E21F-EFE1-4D29-A164-692109D86C4A}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {3237E21F-EFE1-4D29-A164-692109D86C4A}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {37CEA85B-7E35-4D48-A54B-2451B8842B99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {37CEA85B-7E35-4D48-A54B-2451B8842B99}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {37CEA85B-7E35-4D48-A54B-2451B8842B99}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {37CEA85B-7E35-4D48-A54B-2451B8842B99}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {D2E5E14F-5F9E-476F-82F1-3B9FA3793D71} 30 | EndGlobalSection 31 | EndGlobal 32 | --------------------------------------------------------------------------------