-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
improve support for Functional Components and React Hooks
- Loading branch information
1 parent
92f8c65
commit ff296a9
Showing
9 changed files
with
326 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
using Signum.Utilities; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Text.RegularExpressions; | ||
|
||
namespace Signum.Engine.CodeGeneration | ||
{ | ||
public class ReactHookConverter | ||
{ | ||
public void ConvertFilesToHooks() | ||
{ | ||
while (true) | ||
{ | ||
IEnumerable<string> files = GetFiles(); | ||
|
||
if (files == null) | ||
return; | ||
|
||
foreach (var file in files) | ||
{ | ||
Console.Write(file + "..."); | ||
|
||
var content = File.ReadAllText(file); | ||
|
||
var converted = SimplifyFile(content); | ||
|
||
File.WriteAllText(file, converted); | ||
} | ||
} | ||
} | ||
|
||
public virtual IEnumerable<string> GetFiles() | ||
{ | ||
string folder = GetFolder(); | ||
|
||
var files = Directory.EnumerateFiles(folder, "*.tsx", SearchOption.AllDirectories); | ||
|
||
return SelectFiles(folder, files); | ||
} | ||
|
||
public virtual string GetFolder() | ||
{ | ||
CodeGenerator.GetSolutionInfo(out var solutionFolder, out var solutionName); | ||
|
||
var folder = $@"{solutionFolder}\{solutionName}.React\App"; | ||
return folder; | ||
} | ||
|
||
public virtual IEnumerable<string> SelectFiles(string folder, IEnumerable<string> files) | ||
{ | ||
var result = files.Select(a => a.After(folder)).ChooseConsoleMultiple(); | ||
|
||
if (result.IsEmpty()) | ||
return null; | ||
|
||
return result.Select(a => folder + a); | ||
} | ||
|
||
public virtual string SimplifyFile(string content) | ||
{ | ||
HashSet<string> hookImports = new HashSet<string>(); | ||
|
||
|
||
var componentStarts = Regex.Matches(content, @"^(?<export>export )?(?<default>default )?class (?<className>\w+) extends React\.Component<(?<props>.*?)>\s*{\s*\r\n", RegexOptions.Multiline).Cast<Match>(); | ||
|
||
foreach (var m in componentStarts.Reverse()) | ||
{ | ||
var endMatch = new Regex(@"^}\s*$", RegexOptions.Multiline).Match(content, m.EndIndex()); | ||
|
||
var simplifiedContent = SimplifyClass(content.Substring(m.EndIndex(), endMatch.Index - m.EndIndex()), hookImports); | ||
|
||
string newComponent = m.Groups["export"].Value + m.Groups["default"].Value + "function " + m.Groups["className"].Value + "(p : " + m.Groups["props"].Value + "){\r\n" | ||
+ simplifiedContent | ||
+ endMatch.Value; | ||
|
||
|
||
content = content.Substring(0, m.Index) + newComponent + content.Substring(endMatch.EndIndex()); | ||
} | ||
|
||
|
||
if (hookImports.Any()) | ||
{ | ||
var lastImport = Regex.Matches(content, "^import.*\r\n", RegexOptions.Multiline).Cast<Match>().Last(); | ||
|
||
return content.Substring(0, lastImport.EndIndex()) + | ||
$"import {{ {hookImports.ToString(", ")} }} from '@framework/Hooks'\r\n" + | ||
content.Substring(lastImport.EndIndex()); | ||
} | ||
else | ||
{ | ||
|
||
return content; | ||
} | ||
} | ||
|
||
public virtual string SimplifyClass(string content, HashSet<string> hookImports) | ||
{ | ||
HashSet<string> hooks = new HashSet<string>(); | ||
|
||
var matches = Regex.Matches(content, @"^ (?<text>\w.+)\s*\r\n", RegexOptions.Multiline).Cast<Match>().ToList(); | ||
var endMatch = new Regex(@"^ };?\s*$", RegexOptions.Multiline).Matches(content).Cast<Match>().ToList(); | ||
|
||
var pairs = matches.Select(m => new { isStart = true, m }) | ||
.Concat(endMatch.Select(m => new { isStart = false, m })) | ||
.OrderBy(p => p.m.Index) | ||
.BiSelect((start, end) => new { start, end }) | ||
.Where(a => a.start.isStart && !a.end.isStart) | ||
.Select(a => new { start = a.start.m, end = a.end.m }) | ||
.ToList(); | ||
|
||
string render = null; | ||
|
||
foreach (var p in pairs.AsEnumerable().Reverse()) | ||
{ | ||
var methodContent = content.Substring(p.start.EndIndex(), p.end.Index - p.start.EndIndex()); | ||
|
||
var simplifiedContent = SimplifyMethod(methodContent, hooks, hookImports); | ||
|
||
if (p.start.Value.Contains("render()")) | ||
{ | ||
render = simplifiedContent.Lines().Select(l => l.StartsWith(" ") ? l.Substring(2) : l).ToString("\r\n"); | ||
|
||
content = content.Substring(0, p.start.Index) + content.Substring(p.end.EndIndex()); | ||
} | ||
else | ||
{ | ||
string newComponent = ConvertToFunction(p.start.Value) + simplifiedContent + p.end.Value; | ||
|
||
content = content.Substring(0, p.start.Index) + newComponent + content.Substring(p.end.EndIndex()); | ||
} | ||
} | ||
|
||
return hooks.ToString(s => s + ";\r\n", "").Indent(2) + content + render; | ||
|
||
} | ||
|
||
public virtual string ConvertToFunction(string value) | ||
{ | ||
{ | ||
var lambda = Regex.Match(value, @"(?<ident> *)(?<name>\w+) *= *\((?<params>.*)\) *=> *{"); | ||
if (lambda.Success) | ||
return $"{lambda.Groups["ident"].Value}function {lambda.Groups["name"].Value}({lambda.Groups["params"].Value}) {{\r\n"; | ||
} | ||
|
||
{ | ||
var method = Regex.Match(value, @"(?<ident> *)(?<name>\w+) *\((?<params>.*)\) *{"); | ||
if (method.Success) | ||
return $"{method.Groups["ident"].Value}function {method.Groups["name"].Value}({method.Groups["params"].Value}) {{\r\n"; | ||
} | ||
return value; | ||
} | ||
|
||
public virtual string SimplifyMethod(string methodBody, HashSet<string> hooks, HashSet<string> hooksImports) | ||
{ | ||
methodBody = methodBody.Replace("this.props", "p"); | ||
|
||
if(methodBody.Contains("this.forceUpdate")) | ||
{ | ||
hooksImports.Add("useForceUpdate"); | ||
hooks.Add("const forceUpdate = useForceUpdate()"); | ||
methodBody = methodBody.Replace("this.forceUpdate", "forceUpdate"); | ||
} | ||
methodBody = methodBody.Replace("this.state.", ""); | ||
methodBody = methodBody.Replace("this.", ""); | ||
|
||
return methodBody; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import * as React from 'react' | ||
import { FindOptions, ResultTable } from './Search'; | ||
import * as Finder from './Finder'; | ||
import * as Navigator from './Navigator'; | ||
import { Entity, Lite, liteKey } from './Signum.Entities'; | ||
import { EntityBase } from './Lines/EntityBase'; | ||
import { Type } from './Reflection'; | ||
|
||
export function useForceUpdate(): () => void { | ||
return React.useState()[1] as () => void; | ||
} | ||
|
||
export function useAPI<T>(defaultValue: T, key: ReadonlyArray<any> | undefined, makeCall: (signal: AbortSignal) => Promise<T>): T { | ||
|
||
const [data, updateData] = React.useState<T>(defaultValue) | ||
|
||
React.useEffect(() => { | ||
var abortController = new AbortController(); | ||
|
||
updateData(defaultValue); | ||
|
||
makeCall(abortController.signal) | ||
.then(result => !abortController.signal.aborted && updateData(result)) | ||
.done(); | ||
|
||
return () => { | ||
abortController.abort(); | ||
} | ||
}, key); | ||
|
||
return data; | ||
} | ||
|
||
export function useQuery(fo: FindOptions | null): ResultTable | undefined | null { | ||
return useAPI(undefined, [fo && Finder.findOptionsPath(fo)], signal => | ||
fo == null ? Promise.resolve<ResultTable | null>(null) : | ||
Finder.getQueryDescription(fo.queryName) | ||
.then(qd => Finder.parseFindOptions(fo!, qd)) | ||
.then(fop => Finder.API.executeQuery(Finder.getQueryRequest(fop), signal))); | ||
} | ||
|
||
|
||
export function useFetchAndForget<T extends Entity>(lite: Lite<T> | null): T | null | undefined { | ||
return useAPI(undefined, [lite && liteKey(lite)], signal => | ||
lite == null ? Promise.resolve<T | null>(null) : | ||
Navigator.API.fetchAndForget(lite)); | ||
} | ||
|
||
|
||
export function useFetchAndRemember<T extends Entity>(lite: Lite<T> | null): T | null | undefined { | ||
|
||
const forceUpdate = useForceUpdate(); | ||
React.useEffect(() => { | ||
if (lite != null && lite.entity != null) | ||
Navigator.API.fetchAndRemember(lite) | ||
.then(() => forceUpdate()) | ||
.done(); | ||
}, [lite && liteKey(lite)]); | ||
|
||
|
||
if (lite == null) | ||
return null; | ||
|
||
if (lite.entity == null) | ||
return undefined; | ||
|
||
return lite.entity; | ||
} | ||
|
||
export function useFetchAll<T extends Entity>(type: Type<T>): T[] | undefined { | ||
return useAPI(undefined, [], signal => Navigator.API.fetchAll(type)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
ff296a9
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
React Hooks 2
This commit (and a children commits) makes the support of React Functional Components (required to use React Hooks) first class in Signum React.
The improvements are:
ReactHookConverter
inCodeGenerator
to help convert Class components to Function components. Note: only works with components correctly formatted, and manual fixes are required when the component has state.react.function.snippet
activable withreactFunction
.GenerateFunctionalComponent
(with default totrue
) inReactCodeGenerator
viewOverrides
for Functional Components.Hooks.ts
withuseForceUpdate
,useAPI
,useQuery
,useFetchAndForget
,useFetchAndRemember
,useFetchAll
for now...With this changes, React Functional Components become the default way of building components for entities, but not the only because they still have one important limitation: You can not take a
ref
of a functional component to call methods on it.So slowly, maybe more components will be translated to functional components, but not sure if
SearchControl
, or components that implementeIRenderButtons
orISimpleFilterBuilder
will ever be.Enjoy!
ff296a9
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.