Solidjs를 사용한 멋진 양식

나는 최근에 Solidjs 에 빠져들기 시작했습니다. 자바스크립트 라이브러리는 React처럼 보이지만 훨씬 빠르며 감히 더 나은 API를 가지고 있습니다. React와 달리 Solidjs 구성 요소 함수는 구성 요소가 초기화될 때 한 번만 호출되고 다시는 호출되지 않습니다.

저는 Solidjs의 장점을 활용하고 사용자 입력 양식을 지원하기 위해 최소 9kb의 압축 라이브러리를 구축하기로 결정했습니다. rx-controls-solid . 우리가 무엇을 할 수 있는지 살펴보겠습니다(Solidjs에 대한 소개가 필요한 경우 참고).
  • 업데이트(3/12/22): 이 라이브러리를 직접 사용한지 얼마 되지 않아 일반용으로 사용할 준비가 되지 않은 것 같습니다. 몇 가지 복잡한 시나리오에서 경쟁 조건이 발생했습니다. 이를 수정하려면 구현에 대한 몇 가지 근본적인 변경이 필요합니다. 재미/작은 프로젝트의 경우 잘 작동해야 합니다. 또한 일반적인 인체 공학 및 개념에 매우 만족합니다.

  • typescript에서 간단한TextField 구성 요소를 만들어 보겠습니다.

    import { withControl, FormControl } from 'rx-controls-solid';
    
    export const TextField = withControl((props) => {
      // prop.control is static for the lifetime of the component
      const control = props.control as FormControl<string | null>;
    
      return (
        <label>
          <span class='input-label'>{props.label}</span>
    
          <input
            type="text"
            value={control.value}
            oninput={(e) => {
              control.markDirty(true);
              control.setValue(e.currentTarget.value || null);
            }}
            onblur={() => control.markTouched(true)}
            placeholder={props.placeholder}
          />
        </label>
      );
    });
    


    이 구성 요소는 사용자가 변경했는지touched(공지onblur 콜백) 사용자가 변경했는지(oninput) 추적합니다. 사용자가 값을 변경하면 사용자가 값을 변경했는지 추적하기 위해 컨트롤을 dirty로 표시합니다. 또한 입력과 자리 표시자에 레이블을 설정할 수 있습니다. 꽤 간단한 물건.

    그러나 텍스트 필드는 단독으로 거의 사용되지 않습니다. 주소 정보를 수집하는 구성 요소를 만들고 싶습니다. 여기에는 Street , City , StatePostcode 를 요청하는 작업이 포함됩니다. TextField 구성 요소를 사용하여 AddressForm 를 생성합니다.

    import { withControl, FormGroup, FormControl } from 'rx-controls-solid';
    import { toSignal } from './utils';
    
    const controlFactory = () => 
        new FormGroup({
          street: new FormControl<string | null>(null),
          city: new FormControl<string | null>(null),
          state: new FormControl<string | null>(null),
          zip: new FormControl<string | null>(null),
        });
    
    export const AddressForm = withControl({
      controlFactory,
      component: (props) => {
        const control = props.control;
    
        const isControlValid = toSignal(control.observe('valid'));
        const isControlTouched = toSignal(control.observe('touched'));
        const isControlDirty = toSignal(control.observe('dirty'));
    
        return (
          <fieldset classList={{
            "is-valid": isControlValid(),
            "is-invalid": !isControlValid(),
            "is-touched": isControlTouched(),
            "is-untouched": !isControlTouched(),
            "is-dirty": isControlDirty(),
            "is-clean": !isControlDirty(),
          }}>
            <TextField label="Street" controlName="street" />
            <TextField label="City" controlName="city" />
            <TextField label="State" controlName="state" />
            <TextField label="Postcode" controlName="zip" />
          </fieldset>
        );
      },
    });
    


    주소 양식 자체도 래핑됩니다withControl(). 이렇게 하면 AddressForm를 더 큰 상위 양식의 양식 구성 요소로도 사용할 수 있습니다.
    AddressForm가 기본값FormGroup이 아닌 FormControl 컨트롤을 사용하기를 원하므로 컨트롤을 초기화하는 controlFactory 함수를 제공합니다.

    const controlFactory = () => 
        new FormGroup({
          street: new FormControl<string | null>(null),
          city: new FormControl<string | null>(null),
          state: new FormControl<string | null>(null),
          zip: new FormControl<string | null>(null),
        });
    
    export const AddressForm = withControl({
      controlFactory,
      component: (props) => {
        const control = props.control;
    
        const isControlValid = toSignal(control.observe('valid'));
        // continued...
    


    AddressForm 컨트롤을 TextField's 컨트롤에 연결하기 위해 해야 할 일은 controlName="street" 속성을 사용하여 부모의 어떤 FormControl이 자식 TextField와 연결되어야 하는지 지정하는 것이었습니다.

    <TextField label="Street" controlName="street" />
    <TextField label="City" controlName="city" />
    


    또한 AddressForm가 유효한지/무효한지, 편집된/편집되지 않은지, 터치된/건드리지 않은지 여부에 따라 CSS 클래스를 적용하도록 구성 요소를 설정했습니다. 실제로 CSS 클래스를 쉽게 적용할 수 있는 도우미 기능이 있지만 교육을 위해 이 예제에서는 사용하지 않았습니다.

    AddressForm 구성 요소를 더 큰 양식에 연결하고 싶다고 가정해 보겠습니다. 그것도 쉽습니다!

    // factory for initializing the `MyLargerForm` `FormGroup`
    const controlFactory = () => 
        new FormGroup({
          firstName: new FormControl<string | null>(null),
          address: new FormGroup({
            street: new FormControl<string | null>(null),
            city: new FormControl<string | null>(null),
            state: new FormControl<string | null>(null),
            zip: new FormControl<string | null>(null),        
          }),
        });
    
    // the form component itself
    export const MyLargerForm = withControl({
      controlFactory,
      component: (props) => {
        const control = props.control;
    
        // because we can
        const lastNameControl = new FormControl<string | null>(null);
    
        return (
          <form>
            <fieldset>
              <TextField label="First name" controlName="firstName" />
              <TextField label="Last name" control={lastNameControl} />
            </fieldset>
    
            <AddressForm controlName="address" />
          </form>
        );
      },
    });
    


    그리고 몇 단계만 거치면 매우 강력하고 매우 구성 가능한 양식 구성 요소 집합을 갖게 됩니다. TextField 구성 요소에 변경 사항이 발생하면 해당 변경 사항이 위쪽으로 흐르고 상위FormGroup 구성 요소를 자동으로 업데이트합니다.

    이러한 변경 사항을 쉽게 듣고 부모를 통해 응답할 수 있습니다.

    예를 들어, 양식의 어떤 부분이 터치될 때 수신 대기하려면 touched 속성 상태/변경 사항을 구독하면 됩니다.

    control.observe('touched').subscribe(v => {/* ... */})
    


    특히 "firstName"컨트롤이 터치되었을 때 수신하려면

    // this is similar to control.controls.firstName.touched
    control.observe('controls', 'firstName', 'touched')
    // or
    control.get('firstName').observe('touched')
    


    다음은 더 복잡한 고급 예입니다. 값 변경을 수신하고, 변경 비율을 디바운스하고, 유효성 검사를 수행하고, 유효성 검사가 완료될 때까지 기다리는 동안 control를 보류 중으로 표시하려면 다음을 수행할 수 있습니다. firstName 컨트롤에 오류를 설정하면 "이름"TextField이 잘못된 것으로 표시됩니다(점수!).

    import { interval } from 'rxjs';
    import { switchMap, tap, take } from 'rxjs/operators';
    import { myCustomValidationService } from './my-validation-service';
    
    export const MyLargerForm = withControl({
      // ...hiding the controlFactory boilerplate...
      component: (props) => {
        const control = props.control;
        const firstName = control.get('firstName');
    
        const sub = control.observe('value', 'firstName').pipe(
          tap(() => firstName.markPending(true)),
          switchMap(v => interval(500).pipe(
            take(1),
            switchMap(() => myCustomValidationService(v)),
            tap(() => firstName.markPending(false)),
          )),
        ).subscribe(result => {
          if (result.errors) {
            firstName.setErrors({ validationFailed: true });
          } else {
            firstName.setErrors(null);
          }
        });
    
        const onsubmit (e) => {
          e.preventDefault();
          if (control.pending || control.invalid) return;
    
          // do stuff...
        };
    
        onCleanup(() => sub.unsubscribe());
    
        return (
          <form onsubmit={onsubmit}>
            <fieldset>
              <TextField label="First name" controlName="firstName" />
              <TextField label="Last name" control={lastNameControl} />
            </fieldset>
    
            <AddressForm controlName="address" />
          </form>
        );
      },
    });
    


    이것은 실제로 할 수 있는 일의 표면을 긁는 것입니다rx-controls-solid. Check out the repo to read the documentation and learn more 또는 this codesandbox 을 사용하여 라이브러리를 가지고 놀 수 있습니다.

    리포지토리를 확인하세요


  • https://gitlab.com/john.carroll.p/rx-controls
  • 좋은 웹페이지 즐겨찾기