Storybook: Changer les boutons via des actions [idée]

Créé le 9 juil. 2018  ·  55Commentaires  ·  Source: storybookjs/storybook

Je viens de commencer à utiliser le livre de contes et jusqu'à présent, je l'adore. Une chose avec laquelle j'ai eu du mal est de savoir comment traiter les composants "sans état" purs qui nécessitent que l'état soit contenu dans un parent. Par exemple, j'ai une case à cocher qui prend un accessoire checked . Cliquer sur la case à cocher ne change pas son état, mais déclenche plutôt un onChange et attend de récupérer un accessoire checked à jour. Il ne semble pas y avoir de documentation sur les meilleures pratiques pour gérer ces types de composants, et les suggestions dans des problèmes tels que https://github.com/storybooks/storybook/issues/197 ont été de créer un composant wrapper ou de ajouter un autre module complémentaire. Je préférerais ne pas créer de wrapper de composant si je peux l'aider, car j'aimerais garder mes histoires aussi simples que possible.

Une idée que j'ai eu à gérer est de câbler le module complémentaire actions avec knobs , permettant aux boutons d'être basculés par programmation via des actions. Comme je l'ai dit, je suis tout nouveau dans le livre de contes, donc je ne sais pas si c'est même faisable, mais je voulais au moins soulever la suggestion.

C'est un point d'achoppement pour moi dans la mise en œuvre des histoires, et j'imagine que cela pourrait l'être aussi pour d'autres. Si créer un composant wrapper est vraiment la meilleure chose à faire, peut-être qu'une documentation pourrait être ajoutée pour clarifier cela, et comment l'accomplir ?

knobs feature request todo

Commentaire le plus utile

Nous avons presque terminé avec la 5.3 (version début janvier) et nous étudions une réécriture des boutons pour la 6.0 (version fin mars) qui résoudra un tas de problèmes de boutons de longue date. Je vais voir si on peut y travailler ! Merci pour votre patience!

Tous les 55 commentaires

Une sorte d'idée similaire a été introduite dans ce PR #3701, qui était controversée (et n'a pas été fusionnée).

Nous pouvons rouvrir une discussion à ce sujet et écouter les suggestions de l'API =).

Ah merci, je n'avais pas vu ce PR. Merci @aherriot d' avoir mis cela en place, il semble que nous ayons les mêmes pensées.

Avant de plonger dans une API, il semble que le concept fondamental doive être discuté et accepté. L'un des commentaires dans le PR était celui-ci de @Hypnosphi :

Je n'aime pas le fait qu'il présente plusieurs sources de vérité (rappels de composants et boutons d'interface utilisateur)

Il me semble que l'idée n'est pas d'introduire différentes sources de vérité, mais plutôt de permettre le maintien d'une _seule_ source de vérité pour l'état des composants : les boutons. Toutes les autres approches que j'ai vues suggérées (composants wrapper, add-ons d'état, recompose) introduiraient en fait une autre source de vérité. Dans mon exemple de case à cocher, je ne serais pas en mesure d'avoir un bouton pour checked et d'autoriser également un composant wrapper à fournir la prop checked . Je considère le panneau de commande des boutons comme le parent du composant, sauf qu'actuellement, il ne peut obtenir aucun rappel du composant, ce qui est en quelque sorte unilatéral et non la façon dont les applications de réaction sont normalement construites.

En permettant aux boutons d'être contrôlés par programmation, l'histoire peut être isolée uniquement du composant en cours de démonstration, laissant le mécanisme de gestion de l'état réel opaque, comme il devrait l'être pour un simple composant de présentation comme une case à cocher. La case à cocher elle-même ne se soucie pas de la façon dont elle obtient ses accessoires, elle peut être connectée à redux, son parent peut utiliser setState , peut-être qu'il utilise withState de recompose, ou peut-être que les accessoires sont contrôlés par boutons de livre de contes.

Quoi qu'il en soit, comme je l'ai dit, je suis très nouveau dans ce domaine, donc je ne partage que mes pensées intuitives. Si je ne suis pas sur la base et qu'il existe une « meilleure pratique » généralement acceptée pour traiter ce type de composants sans état purs, peut-être que quelqu'un peut m'indiquer un bon exemple que je peux suivre ?

Salut :)

Je veux juste ajouter que je suis tombé dessus car mes composants reçoivent s'ils devaient afficher leurs dispositions mobiles (ou non) à partir d'accessoires, et mon objectif était de lier le changement de fenêtre à mon bouton, et cela aurait été vraiment sympa ;)

Je voulais juste ajouter mon cas d'utilisation ici, et comme @IanVS, même un rappel aurait été bien (donc si je bascule mes accessoires isMobile, je pourrais déclencher un changement de fenêtre)

J'ai entendu plusieurs personnes exprimer leur intérêt pour une méthode de mise à jour de l'état des boutons. Si nous pouvons nous mettre d'accord sur une bonne architecture, je pense que cela sera utile à beaucoup de gens. D'autant plus qu'il s'agit d'une fonctionnalité supplémentaire que les gens peuvent choisir d'utiliser ou non et parce qu'elle n'affecte pas négativement ceux qui ne veulent pas l'utiliser.

@Hypnosphi , vous aviez des objections sur l'implémentation précédente, WDYT ?

Commenter ici pour garder ce problème ouvert. Je suis toujours curieux de savoir ce que @Hypnosphi a à dire sur @igor-dv précédemment proposé ici.

Il me semble que l'idée n'est pas d'introduire différentes sources de vérité, mais plutôt de permettre de maintenir une seule source de vérité pour l'état des composants : les boutons. Toutes les autres approches que j'ai vues suggérées (composants wrapper, add-ons d'état, recompose) introduiraient en fait une autre source de vérité. Dans mon exemple de case à cocher, je ne serais pas en mesure d'avoir un bouton pour coché, et d'autoriser également un composant wrapper à fournir l'accessoire coché. Je considère le panneau de commande des boutons comme le parent du composant, sauf qu'actuellement, il ne peut obtenir aucun rappel du composant, ce qui est en quelque sorte unilatéral et non la façon dont les applications de réaction sont normalement construites.

Cela me semble raisonnable. Parlons de l'API

Ce sera une grande fonctionnalité. Je viens de faire face à ce même besoin ici avec un composant contrôlé très basique et je l'avais déjà rencontré auparavant dans d'autres composants également. Merci de l'avoir regardé.

Suivre la syntaxe actuelle semble un petit défi. Peut-être que vous pourriez utiliser quelque chose comme :

const {value: name, change: setName} = text('Name', 'Kent');

@brunoreis , je crains que la modification de la signature de retour des boutons ne soit un changement

import {boolean, changeBoolean} from '@storybook/addon-knobs/react';

stories.add('custom checkbox', () => (
    <MyCheckbox
        checked={boolean('checked', false)}
        onChange={(isChecked) => changeBoolean('checked', isChecked)} />
));

Où le rappel onChange pourrait même permettre le curry, de sorte que cela fonctionnerait également :

onChange={(isChecked) => changeBoolean('checked')(isChecked)}

// which of course simplifies down to
onChange={changeBoolean('checked')}

L'important est que le premier argument doit être le même que l'étiquette du bouton qui doit être changé. Je pense que cela permettrait aux utilisateurs d'opter pour ce comportement sans rien changer à la façon dont les boutons sont actuellement utilisés. (À moins que les étiquettes ne soient actuellement pas obligées d'être uniques ? Je suppose qu'elles le sont…)

@IanVS , c'est sympa. Je suis d'accord avec toi pour ne pas changer la façon dont les choses fonctionnent. Je n'ai pas trouvé le moyen de le faire car je n'ai pas pensé à utiliser l'étiquette comme clé. Cela pourrait fonctionner. Voyons ce que @Hypnosphi a en tête.

changer la signature de retour des boutons serait un changement décisif

Techniquement, ce n'est pas un problème pour le moment. Nous avons une version majeure à venir. Mais il serait en effet préférable de permettre une certaine rétrocompatibilité.

J'aime l'idée du soutien curry.

À moins que les étiquettes ne soient actuellement obligatoires pour être uniques ?

Oui, ils le sont, et en fait, ils sont uniques à travers différents types. Il ne devrait donc pas y avoir besoin d'exporter change<Type> séparément, juste change devrait suffire. C'est essentiellement ce qui a été fait dans https://github.com/storybooks/storybook/pull/3701
Je pense que je vais juste rouvrir ce PR pour laisser @aherriot le terminer avec un peu d'aide de votre côté

On peut avoir ça :

const {value: name, change: setName} = text('Name', 'Kent');

sans avoir à modifier le type de retour de text .

Les fonctions Javascript sont des objets et peuvent donc avoir des propriétés.
L'objet peut être déstructuré.

screen shot 2018-09-19 at 00 08 35

@ndelangen la fonction text est bien un objet, mais sa valeur de retour ne l'est pas. Cela ne fonctionnera pas dans votre exemple :

const { foo, bar } = x() // note the parens

Nous pourrions avoir quelque chose comme ça (le nom est discutable):

const {value: name, change: setName} = text.mutable('Name', 'Kent');

pourquoi ça ne marche pas ?

const { foo, bar } = x() // note the parens

Le retour de x est une fonction, qui peut aussi avoir des propriétés.

screen shot 2018-09-23 at 14 12 28

Je parlais de x = () => {} de votre exemple original.

Si nous faisons en sorte que text renvoie une fonction, les utilisateurs devront modifier leur code :

// Before
<Foo bar={text('Bar', '')}>

// After
<Foo bar={text('Bar', '')()}>
                         ^^ this

je vois

Qu'en est-il de la suggestion de @IanVS à ce sujet ?

ce serait cool d'avoir cette fonction.

qu'en est-il de "destruct", comme avec "react hooks" ? :

const [foo, setFoo] = useString('foo', 'default');

Je suis venu dire la même chose que @DimaRGB
Serait capable de préserver la syntaxe existante et d'ajouter des appels de type crochet, par exemple :

.add('example', () => {
  const [selected, setSelected] = useBool(false);

  return <SomeComponent selected={selected} onSelected={setSelected} />
})

Le besoin occasionnel étant que les composants de réaction ne soient pas censés mettre à jour leur propre props , mais disent plutôt à leur parent qu'ils doivent être modifiés. Parfois, un composant contient un élément censé basculer ses accessoires (je me suis heurté à cela parce que parfois un menu de la barre de navigation que j'ai doit s'ouvrir si un élément enfant est cliqué).

@IanVS J'ai exactement la même situation où j'ai un composant à bascule qui n'est mis à jour que via les accessoires parents et une fois que l'utilisateur d'une histoire de livre de contes le bascule, l'état de l'interface utilisateur est incompatible avec ce qui est affiché dans le panneau des boutons. Je l'ai contourné en utilisant une méthode privée de @storybook/addons-knobs - une suggestion plus simple serait de fournir une API officielle pour faire quelque chose comme :

import { manager } from '@storybook/addons-knob/dist/registerKnobs.js'

const { knobStore } = manager
// The name given to your knob - i.e:  `select("Checked", options, defaultValue)`
knobStore.store['Checked'].value = newValue
// Danger zone! _mayCallChannel() triggers a re-render of the _whole_ knobs form.
manager._mayCallChannel()

@erickwilder J'ai essayé votre approche mais cela n'a jamais semblé mettre à jour la forme du bouton (même lorsqu'il a mis à jour les accessoires fournis au composant).

ÉDITER:

grattez ça; Je n'ai tout simplement pas vu ces mises à jour car j'utilisais des options () avec des cases à cocher qui fonctionnent apparemment de manière "non contrôlée". En passant à la sélection multiple et en utilisant cette méthode dans un rappel, j'ai obtenu les valeurs mises à jour sous la forme du bouton :

// Ditch when https://github.com/storybooks/storybook/issues/3855 gets resolved properly.
function FixMeKnobUpdate(name, value) {
    addons.getChannel().emit(CHANGE, { name, value });
}

J'ai peut-être omis certains détails de ma configuration:

  • Nous utilisons Vue.js avec Storybook
  • Le code mutant la valeur et déclenchant le re-rendu a été fait à l'intérieur d'une méthode si le composant d'habillage de l'histoire.

Je ne sais pas si une telle approche fonctionnerait pour tout le monde.

Je suis tout à fait d'accord avec @IanVS , j'adore les livres de contes mais la possibilité de mettre à jour les boutons via des actions me manque vraiment. Dans mon cas, j'ai deux composants individuels A et B mais dans l'une de mes histoires, je veux montrer une composition utilisant les deux, de sorte que chaque fois que je change d'IA, émette une action qui modifierait B et vice versa.

J'ai essayé le hack de @erickwilder avec ma configuration (Angular 7), mais chaque fois que j'essaie de mettre à jour le magasin de boutons, je reçois l'erreur suivante:

Erreur : ne devrait pas être dans la zone angulaire, mais c'est le cas !

J'ai essayé d'exécuter cela en dehors d'Angular mais je n'y suis pas parvenu... Dans le pire des cas, je vais créer un troisième composant C qui sera un wrapper de A et B.

La solution temporaire @erickwilder devrait fonctionner pour n'importe quel framework car elle n'est pas liée au framework/library (react, angular, vue, quoi d'autre) mais au rendu de l'addon Knobs.

A travaillé pour nous en utilisant React de la même manière que mentionné ci -

+1 pour ajouter une méthode visible publiquement à déclencher, contenant le nom du bouton comme chaîne à mettre à jour et une nouvelle valeur, par exemple :

import { manager } from "@storybook/addon-knobs"

manager.updateKnob(
  propName, // knobs property, example from above "Checked"
  newValue, // new value to set programmatically, ex. true
)

J'aimerais vraiment que cela soit officiellement soutenu.

En attendant, avec Storybook v5, je devais opter pour cette solution terriblement bidon :

window.__STORYBOOK_ADDONS.channel.emit('storybookjs/knobs/change', {
  name: 'The name of my knob',
  value: 'the_new_value'
})

??

??

Connexe : #6916

Ce serait cool... Surtout parce que les modaux.

Je pense que ce serait utile.

Exemple:

storiesOf('input', module)
  .addDecorator(withKnobs)
  .add('default', () => <input type={text('type', 'text')} value={text('value', '')} disabled={boolean('disabled', false)} placeholder={text('placeholder', '')} onChange={action('onChange')} />)

Cela fonctionne actuellement, mais il semble naturel que nous souhaitions câbler la valeur cible de l'événement onChange à la valeur définie dans les props de l'élément testé. Cela démontrerait mieux l'application de l'élément.

ce serait utile

+1 à @mathieuk/ @raspo pour avoir

Nous pouvons également y accéder d'une autre manière. Créer quelques méthodes génériques dans un module helpers ?

import addons from "@storybook/addons";

export const emitter = (type, options) => addons.getChannel().emit(type, options);

export const updateKnob = (name, value) => (
  emitter("storybookjs/knobs/change", { name, value })
);

Et appeler au besoin dans les histoires...

import { text } from "@storybook/addon-knobs";
import { updateKnob } from 'helpers';

// ...
const value = text("value", "Initial value");
<select
  value={value}
  onChange={({ target }) => updateKnob("value", target.value)}
>

... mais ressemble toujours à une solution de contournement de liaison bidirectionnelle géniale à quelque chose qui pourrait être intégré à l'API des boutons

Une mise à jour sur ce qui précède ? Serait une fonctionnalité très utile

Cette fonctionnalité en cours d'implémentation nous permettrait de faire assez facilement des boutons dépendants croisés très puissants. Imaginez faire une histoire de composant de pagination si vous pouviez avoir un bouton mis à jour dynamiquement en fonction d'un autre, avec quelque chose comme ça dans votre histoire :

const resultsCount = number(
    'Results count',
    currentPage, {
        max: 100,
        min: 0,
        range: true
    }
);
const resultsArray: React.ReactNode[] = new Array(resultsCount)
    .fill(true)
    .map((_, idx) => <div key={idx + 1}>Test Pagination Result #{idx + 1}</div>);
const childrenPerPage = 10;
const currentPage = number(
    'Current page index',
    0, {
        max: Math.ceil(resultsCount / childrenPerPage) - 1,
        min: 0,
        range: true
    }
);

J'ai essayé de faire essentiellement ceci, en espérant que la plage maximale de mon bouton currentPage mettrait à jour dynamiquement lors de l'augmentation du bouton resultsCount par incrément de 10, mais il semble que la valeur initiale de chaque bouton est mis en cache au moment de la création et ne s'habitue pas à remplacer les valeurs d'état internes dans les rendus suivants. Avec le code ci-dessus, lorsque j'augmente resultsCount de 10+, je m'attendrais à ce que le max de currentPage augmente également de 1, mais sa valeur reste la même malgré les valeurs sous-jacentes utilisé pour calculer qu'il a changé (le journal Math.ceil(resultsCount / childrenPerPage) - 1 montre la nouvelle valeur attendue).

Nous avons presque terminé avec la 5.3 (version début janvier) et nous étudions une réécriture des boutons pour la 6.0 (version fin mars) qui résoudra un tas de problèmes de boutons de longue date. Je vais voir si on peut y travailler ! Merci pour votre patience!

@shilman Je suis très enthousiasmé par cette fonctionnalité 😄

Moi, @atanasster et @PlayMa256 travaillons sur les bases de cela depuis un certain temps. Il faudra encore quelques itérations pour réussir, mais je suis très confiant que nous pouvons atteindre 100% dans 6.0.0 et révolutionner vraiment les données et la réactivité pour le livre de contes.

Révolutionner? Putain de diggity. Arrête de me taquiner, mon corps est prêt.

+1 pour les modaux :)

Je ne peux pas croire que c'est impossible depuis le début. En attente des mises à jour...

Salut les gens!
Comme solution temporaire, j'ai utilisé dans mon application le code suivant :

import { addons } from '@storybook/addons';
import { CHANGE } from '@storybook/addon-knobs';

const channel = addons.getChannel();

channel.emit(CHANGE, {
  name: 'prop_name',
  value: prop_value,
});

Toujours en attente, cette fonctionnalité sera implémentée nativement.

J'ai résolu ce problème en utilisant observable. J'utilise un livre de contes avec angulaire

`
.add('mise à jour des données du graphique', () => {

const myObservable= new BehaviorSubject([{a: 'a', b: 'b'}]);
return {
  template: '
  <my-component
    myInput: myData,
    (myEvent)="myEventProp($event)"
  ></my-component>
  ',
  props: {
    myData: myObservable,
    myEventProp: $event => {
      myObservable.next([]);
      action('(myEvent)')($event);
    }
  }
};

})
`

@norbert-doofus merci votre exemple m'a aidé

Salut la bande, Nous venons de publier des addons-contrôles dans la version 6.0-beta !

Les commandes sont des boutons portables générés automatiquement qui sont destinés à remplacer les boutons d'extension à long terme.

Veuillez les mettre à niveau et les essayer dès aujourd'hui. Merci pour votre aide et votre soutien à la sortie de cette version stable !

C'est génial! Je ne peux pas vraiment dire en lisant le README (je l'ai peut-être manqué), ces nouveaux controls peuvent-ils être modifiés par programme, quelle était la demande faite dans ce numéro ?

Ils peuvent l'être, bien que je ne sois pas sûr que l'API soit encore officiellement prise en charge (bien que nous fassions exactement cela pour les contrôles dans les addon-docs). Je travaillerai avec @tmeasday pour trouver le meilleur moyen de l'

  • [ ] ajouter updateArgs au contexte de l'histoire ?
  • [ ] rendre le contexte de l'histoire disponible pour un rappel qui est appelé à partir de l'histoire ? ( this.context?.updateArgs(....) )

Pour tous ceux qui s'intéressent aux contrôles mais ne savent pas par où commencer, j'ai créé une procédure pas à pas rapide et simple pour passer d'un nouveau projet CRA à une démo fonctionnelle. Vérifiez-le:

=> Contrôles du livre de contes avec CRA et TypeScript

Il existe également des documents de migration « boutons pour contrôler » dans le fichier README des contrôles :

=> Comment migrer depuis les addon-knobs ?

Salut les gens!
Comme solution temporaire, j'ai utilisé dans mon application le code suivant :

import { addons } from '@storybook/addons';
import { CHANGE } from '@storybook/addon-knobs';

const channel = addons.getChannel();

channel.emit(CHANGE, {
  name: 'prop_name',
  value: prop_value,
});

Toujours en attente, cette fonctionnalité sera implémentée nativement.

Gardez à l'esprit que lorsque vous utilisez groupId vous devrez ajouter l'identifiant du groupe au name comme suit :

 const show = boolean('Show Something', true, 'Group')

channel.emit(CHANGE, {
  name: 'Show Something_Group',
  value: prop_value,
});

De plus, channel est un EventEmitter, vous pouvez donc addListener pour vérifier quels sont les paramètres reçus.

channel.addListener(CHANGE, console.log)

Voici un extrait de code pour toute personne intéressée par la façon de procéder en utilisant addon-controls dans la v6.

import { useArgs } from '@storybook/client-api';

// Inside a story
export const Basic = ({ label, counter }) => {
    const [args, updateArgs] = useArgs();
    return <Button onClick={() => updateArgs({ counter: counter+1 })>{label}: {counter}<Button>;
}

Je ne sais pas si c'est la meilleure API à cet effet mais cela devrait fonctionner. Exemple dans le monorepo :

https://github.com/storybookjs/storybook/blob/next/examples/official-storybook/stories/core/args.stories.js#L34 -L43

Merci @shilman ! Cela a fait l'affaire.

Pour les personnes intéressées, il se trouve que nous avons exactement l'accessoire Checkbox histoire checked qui a lancé tout ce fil. Voici notre nouvelle histoire câblée à l'aide de Storybook 6.0.0-rc.9 :

export const checkbox = (args) => {
  const [{ checked }, updateArgs] = useArgs();
  const toggleChecked = () => updateArgs({ checked: !checked });
  return <Checkbox {...args} onChange={toggleChecked} />;
};
checkbox.args = {
  checked: false,
  label: 'hello checkbox!',
};
checkbox.argTypes = {
  checked: { control: 'boolean' },
};

cb-arg

@shilman J'ai essayé d'utiliser useArgs dans une histoire pour une entrée de texte contrôlée (un cas où nous pourrions normalement utiliser le crochet useState pour mettre à jour la prop value du composant via son événement onChange ). Cependant, j'ai rencontré un problème où chaque fois que l'utilisateur tape un caractère, le composant perd le focus. Est-ce que cela est peut-être dû au fait que l'histoire est actualisée/restituée à chaque fois que nous mettons à jour les arguments ?

Existe-t-il une autre méthode recommandée pour utiliser les arguments/contrôles pour un composant avec une entrée de texte contrôlée ?

C'était avec 6.0.0-rc.13

@jcq Pouvez-vous créer un nouveau problème avec une repro ? Ce n'était pas le cas d'utilisation principal de useArgs , mais c'est certainement un cas que nous aimerions prendre en charge, nous serions donc ravis de l'approfondir.

@shilman Pas de problème — voici le nouveau numéro :
https://github.com/storybookjs/storybook/issues/11657

J'aurais également dû préciser que le comportement errant s'affiche dans Docs, alors qu'il fonctionne correctement en mode Canvas normal.

J'ai résolu ce problème en utilisant observable. J'utilise un livre de contes avec angulaire

`
.add('mise à jour des données du graphique', () => {

const myObservable= new BehaviorSubject([{a: 'a', b: 'b'}]);
return {
  template: '
  <my-component
    myInput: myData,
    (myEvent)="myEventProp($event)"
  ></my-component>
  ',
  props: {
    myData: myObservable,
    myEventProp: $event => {
      myObservable.next([]);
      action('(myEvent)')($event);
    }
  }
};

})
`

Cela a fonctionné pour moi en utilisant Angular mais en passant à myData.value à la ligne 5

Je n'ai pas encore essayé cela (bloqué sur une ancienne version du livre de contes pour l'instant) mais il semble que ce problème puisse être clos maintenant. Merci pour l'excellent travail sur les arguments/contrôles !

Cette page vous a été utile?
0 / 5 - 0 notes