├── .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 |
52 | @(props.User?.LoggedIn ?? false ? "Logout" : "Login")
53 |
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 |
69 | @(props.User?.LoggedIn ?? false ? "Logout" : "Login")
70 |
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 | setDate(date - 1))>Previous Day
9 | setDate(date + 1))>Next Day
10 |
11 |
12 |
13 |
14 | Loading...
15 |
16 |
17 |
18 |
19 |
20 | Date
21 | Temp. (C)
22 | Temp. (F)
23 | Summary
24 |
25 |
26 |
27 | @foreach (var forecast in forecasts)
28 | {
29 |
30 | @forecast.Date.ToShortDateString()
31 | @forecast.TemperatureC
32 | @forecast.TemperatureF
33 | @forecast.Summary
34 |
35 | }
36 |
37 |
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 record
s.
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 | dispatch(state!.IsRunning ? new Stop() : new Start())">
13 | @(state!.IsRunning ? "Stop" : "Restart")
14 |
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 | dispatch(new Increment()))>Up
24 | dispatch(new Decrement()))>Down
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 | setCount(count + 1))>Up
11 | setCount(count - 1))>Down
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 | dispatch(new Increment()))>Up
28 | dispatch(new Decrement()))>Down
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 |
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 |
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 |
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 |
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 + "" + a.tag + ">" }, !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" }, /?[\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*>(?:[^<]|?(?!\\1\\b)" + i + "|" + s("<\\1" + l + "\\s*>(?:[^<]|?(?!\\1\\b)" + i + "|)*\\1\\s*>", 2) + ")*\\1\\s*>|<" + 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+""+env.tag+">"};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:/